Skip to content

test(gl): add tests for iCaptcha transparent retry flow (#167)#168

Open
Gravirei wants to merge 18 commits into
Gitlawb:mainfrom
Gravirei:fix/issue-167-add-icaptcha-client-tests
Open

test(gl): add tests for iCaptcha transparent retry flow (#167)#168
Gravirei wants to merge 18 commits into
Gitlawb:mainfrom
Gravirei:fix/issue-167-add-icaptcha-client-tests

Conversation

@Gravirei

@Gravirei Gravirei commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Add comprehensive tests for the iCaptcha transparent solve-and-retry flow in the gl CLI HTTP client, covering the critical registration path that was blocking new users.

Motivation & context

Closes #167

Node v0.5.0 requires iCaptcha proof for registration, but the released gl v0.3.8 predates the icaptcha-client code. The fix existed on main (transparent solve-and-retry in crates/gl/src/http.rs, crates/icaptcha-client) but lacked test coverage for the retry flow.

Kind of change

  • Bug fix
  • Feature
  • Security fix
  • Docs
  • Tests / CI
  • Refactor (no behavior change)
  • Breaking or protocol change (issue required first)

What changed

  • crates/icaptcha-client/src/lib.rs: Added #[cfg(test)] support in is_https to allow localhost/127.0.0.1 URLs in test builds, enabling mockito-based integration tests for the full iCaptcha retry path.
  • crates/gl/src/http.rs: Added 16 tests:
    • 7 unit tests for icaptcha_cfg (header parsing, defaults, keypair required)
    • 4 tests for send_once (proof header attachment, signing)
    • 3 tests for send_signed (non-iCaptcha 403 passthrough, success, 405 handling)
    • 2 full integration tests using dual mock servers (node + iCaptcha): happy path retry to 201, and retry exhaustion returning 403

How a reviewer can verify

cargo test -p gl -p icaptcha-client
cargo clippy -p gl -p icaptcha-client
cargo fmt -p gl -p icaptcha-client --check

All 266 tests in gl and 12 tests in icaptcha-client pass with no warnings.

Before you request review

  • Scope is one logical change; no unrelated churn
  • cargo test --workspace passes locally (node DB-dependent tests are pre-existing PG auth failures)
  • New behavior is covered by tests (required for fixes)
  • cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings are clean
  • Commit titles use Conventional Commits (test(gl): ...)
  • Docs / .env.example updated if behavior or config changed (or N/A)
  • Checked existing PRs so this isn't a duplicate

Notes for reviewers

The two send_signed integration tests (send_signed_solves_icaptcha_and_retries_to_success, send_signed_returns_403_after_icaptcha_retries_exhausted) use std::env::set_var for GITLAWB_ICAPTCHA_URL. An RAII guard (IcaptchaEnv) ensures cleanup. Tests use #[tokio::test] default single-threaded runtime, so env var races are not a concern.

Summary by CodeRabbit

  • Bug Fixes
    • Profile updates now store content with the correct identifier, improving reliability of saved profile data.
    • Improved request handling for captcha and signed calls, including better retry behavior and more consistent success/failure responses.
    • Local captcha setups can now be used over localhost and 127.0.0.1 when insecure mode is enabled, while still requiring secure connections elsewhere.

Gravirei added 14 commits June 30, 2026 19:52
Gitlawb#126)

The IPFS visibility gate used withheld_blob_oids (a deny-set enumerating
only reachable blobs), so a dangling/unreachable blob was absent from the
set and served in cleartext to anonymous callers. Flip to an allowed-set
(allowed_blob_set_for_caller) that enumerates reachable blobs the caller
may read: a dangling blob has no path, is never in the set, and 404s.
Move store::read_object before the allowed_blob_set_for_caller
spawn_blocking call so random-CID spray against repos with
path-scoped rules cannot trigger full-history git walks on
repos that don't carry the object.
…tion

✓ P3 blocker fixed: cargo fmt applied — the format gate will pass.

  ✓ P3 cleanup resolved: withheld_blob_oids is still used by replication code in repos.rs, so it stays.

  • P2 follow-up: Tree/commit disclosure tracked in Gitlawb#135 — out of scope here.
Copilot AI review requested due to automatic review settings July 9, 2026 14:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Gravirei, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d48162ad-8e21-4f7a-8729-e888a6040430

📥 Commits

Reviewing files that changed from the base of the PR and between d98e202 and 5698c2e.

📒 Files selected for processing (2)
  • crates/gl/src/http.rs
  • crates/icaptcha-client/src/lib.rs
📝 Walkthrough

Walkthrough

This PR relaxes the icaptcha-client trusted-URL check to allow http://127.0.0.1 and http://localhost when GITLAWB_ICAPTCHA_INSECURE is set, adds extensive unit and integration tests for NodeClient's iCaptcha configuration, signing, and retry logic in gl/src/http.rs, and removes a redundant borrow in a profiles.rs format string.

Changes

iCaptcha Client Trust and Testing

Layer / File(s) Summary
Trusted URL check for localhost/insecure mode
crates/icaptcha-client/src/lib.rs
is_https now also trusts http://127.0.0.1 and http://localhost when GITLAWB_ICAPTCHA_INSECURE is set, in addition to standard HTTPS URLs.
Test helpers and icaptcha_cfg unit tests
crates/gl/src/http.rs
Adds Keypair/HeaderMap test helpers and unit tests for icaptcha_cfg covering header detection, defaulting, missing-keypair errors, and unparseable level handling.
send_once and send_signed behavior tests
crates/gl/src/http.rs
Adds async tests verifying send_once proof header attachment and RFC 9421 signing, and send_signed retry/passthrough behavior for 403/405 responses.
iCaptcha solve/retry integration tests
crates/gl/src/http.rs
Adds mockito-based integration tests simulating iCaptcha challenge/answer cycles, verifying successful retry and exhausted-retry 403 outcomes.
Pinata pin object name formatting cleanup
crates/gitlawb-node/src/api/profiles.rs
Removes a redundant & borrow in the format! call building the Pinata pin object name in set_profile.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
    participant GlClient as gl NodeClient
    participant Node as gitlawb-node
    participant Icaptcha as iCaptcha Server

    GlClient->>Node: send_signed(request)
    Node-->>GlClient: 403 icaptcha_proof_required
    GlClient->>Icaptcha: request challenge
    Icaptcha-->>GlClient: challenge
    GlClient->>Icaptcha: submit answer
    Icaptcha-->>GlClient: proof
    GlClient->>Node: retry request with x-icaptcha-proof header
    Node-->>GlClient: 200 success
Loading

Possibly related PRs

  • Gitlawb/node#108: Couples the iCaptcha proof header attachment/insecure-URL trust changes here with the corresponding server-side iCaptcha gate on create_repo/register that validates that header.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the PR’s main change: adding iCaptcha retry-flow tests for gl.
Description check ✅ Passed The description follows the template and fills the required sections with clear test scope, motivation, verification, and notes.
Linked Issues check ✅ Passed The PR covers issue #167 by adding coverage for the transparent iCaptcha solve-and-retry registration flow and its test support.
Out of Scope Changes check ✅ Passed The helper change in icaptcha-client is test support for localhost/mock servers and does not introduce unrelated behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@beardthelion beardthelion added crate:gl gl — the contributor CLI crate:node gitlawb-node — the serving node and REST API kind:test Test coverage or harness labels Jul 9, 2026

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for adding coverage here. The blocker is that the two "full integration" tests don't exercise the iCaptcha retry path they're named for — they fall through to the real production service — so the transparent-retry flow has no real coverage.

I verified by execution: the mockito iCaptcha server is never contacted, and the solve resolves to DEFAULT_URL (https://icaptcha.gitlawb.com) and makes a live network call. Root cause: is_https gates the localhost allowance behind #[cfg(test)], but cfg(test) is not set for icaptcha-client when it's compiled as gl's dependency, so that branch is compiled out during cargo test -p gl. With is_https(http://127.0.0.1:port) false, resolve_solver_url drops both the operator env URL and the advertised header URL and falls back to DEFAULT_URL. Pointing DEFAULT_URL at a bogus host makes both tests fail with a DNS error on {DEFAULT_URL}/v1/challenge; removing the relaxation entirely leaves the tests passing, confirming it's inert.

Findings

  • [P1] Make the integration tests exercise the mock, not the live service. crates/gl/src/http.rs (send_signed_solves_icaptcha_and_retries_to_success, send_signed_returns_403_after_icaptcha_retries_exhausted). Both resolve the solve to https://icaptcha.gitlawb.com and make a real network call — the mock's /v1/challenge and /v1/answer are never hit, and with DEFAULT_URL pointed at a bogus host both fail with a DNS error. So they're network-dependent (fail in a no-egress CI, hit production on every run) and the retry flow is uncovered; the exhaustion test even panics on the live-call error before its assert_eq!(403), so retry-exhaustion is untested. The #[cfg(test)] approach can't work because cfg(test) doesn't propagate to a dependency. Test the localhost solve inside icaptcha-client's own #[cfg(test)] module instead, where the relaxation is live — I confirmed the localhost path is reachable there; alternatively inject the base URL / reqwest client into gl's solve path.

  • [P2] Assert the mocks and the retry count. crates/gl/src/http.rs. Both tests bind their mocks with _ and never call .assert(); the exhaustion test comments "3 node calls" but sets no .expect(3). That silence is exactly why the live-service fallthrough passes unnoticed — add .assert()/.expect(N) so an unreached mock fails loudly.

  • [P2] Drop or rework the is_https #[cfg(test)] relaxation. crates/icaptcha-client/src/lib.rs. It's inert where it's meant to help (removing it changes no test) and nothing depends on it. It does not reach release builds — I confirmed a non-test release build rejects a localhost URL and falls back to DEFAULT_URL — so it's not a security hole, just dead code that implies coverage which isn't there.

  • [P3] The clippy borrow fix in crates/gitlawb-node/src/api/profiles.rs is unrelated to a test(gl) PR and duplicates the identical one-liner already in #156 and #134; drop it here to avoid a conflict/no-op once one of those merges.

Happy to re-review once the tests drive the mock.

@Gravirei Gravirei requested a review from beardthelion July 9, 2026 16:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/icaptcha-client/src/lib.rs (1)

84-105: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Doc comment overstates the safety of this gate; the insecure branch is compiled into production.

Two small points on is_https:

  1. The doc says this avoids "shipping the backdoor to production", but the check is runtime env-gated (GITLAWB_ICAPTCHA_INSECURE), not cfg(test)-gated, so the code path is present in release binaries and merely dormant. Consider rewording so maintainers don't assume it's compiled out.
  2. The insecure branch matches on host only and never asserts scheme() == "http", so any non-https scheme (e.g. ftp://localhost) would also be trusted when the flag is set. Tightening to http closes that gap.
♻️ Optional hardening
     if std::env::var_os("GITLAWB_ICAPTCHA_INSECURE").is_some()
         && parsed
             .as_ref()
-            .map(|p| p.host_str() == Some("127.0.0.1") || p.host_str() == Some("localhost"))
+            .map(|p| {
+                p.scheme() == "http"
+                    && (p.host_str() == Some("127.0.0.1") || p.host_str() == Some("localhost"))
+            })
             .unwrap_or(false)
     {
         return true;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/icaptcha-client/src/lib.rs` around lines 84 - 105, Update the
`is_https` doc comment to avoid implying the insecure path is compiled out of
production; clarify that `GITLAWB_ICAPTCHA_INSECURE` only enables a runtime
escape hatch. Also tighten the insecure branch in `is_https` so it only trusts
`http://localhost` and `http://127.0.0.1`, not any non-https scheme, by
explicitly checking the parsed URL’s scheme alongside the host.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/gl/src/http.rs`:
- Around line 537-538: The fixed hit count is being asserted on the existing
mock after registration, but mockito expectations are set when the mock is
created. Update MockIcaptcha::new to register the challenge and answer mocks
with an expectation of 2 hits, or recreate the mock with the correct count
before the two requests are made, so the assertion matches the actual request
sequence.

---

Nitpick comments:
In `@crates/icaptcha-client/src/lib.rs`:
- Around line 84-105: Update the `is_https` doc comment to avoid implying the
insecure path is compiled out of production; clarify that
`GITLAWB_ICAPTCHA_INSECURE` only enables a runtime escape hatch. Also tighten
the insecure branch in `is_https` so it only trusts `http://localhost` and
`http://127.0.0.1`, not any non-https scheme, by explicitly checking the parsed
URL’s scheme alongside the host.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f6f6b73-1d8f-4622-841e-f4183d09cc89

📥 Commits

Reviewing files that changed from the base of the PR and between 26bc3f6 and d98e202.

📒 Files selected for processing (3)
  • crates/gitlawb-node/src/api/profiles.rs
  • crates/gl/src/http.rs
  • crates/icaptcha-client/src/lib.rs

Comment thread crates/gl/src/http.rs Outdated
@beardthelion beardthelion dismissed their stale review July 9, 2026 19:49

Superseded by 5698c2e: the two blocking findings (tests hitting the live iCaptcha service, no mock assertions) are resolved. Re-reviewing the new head.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. The blocker from the last round is genuinely fixed: the two integration tests now exercise the iCaptcha retry flow against their mock instead of falling through to the live service. I confirmed it by poisoning DEFAULT_URL to a bogus host and re-running — both tests stay green, so the mock is actually consumed and no live call happens; removing the new GITLAWB_ICAPTCHA_INSECURE branch makes them fail with a DNS error on that poisoned host, so the flag is load-bearing. The .expect(N) + .assert() on every mock closes the earlier no-assertions gap. The runtime flag is contained: with it set, a hostile x-icaptcha-url: http://127.0.0.1 (or 169.254.169.254 / [::1] / a userinfo trick) is still rejected and falls back, and the API key stays with the operator origin — I drove those cases through resolve_solver_url and IcaptchaCfg::new.

Three small non-blocking follow-ups:

Findings

  • [P3] Tighten the flag's presence check — crates/icaptcha-client/src/lib.rs:97. std::env::var_os("GITLAWB_ICAPTCHA_INSECURE").is_some() treats any value as enabled, so GITLAWB_ICAPTCHA_INSECURE=0 or =false still turns the insecure loopback path on. A truthy check (== "1" / parse a bool) would honor disable-intent.
  • [P3] The relaxation is now reachable in a non-test build, not just tests — crates/icaptcha-client/src/lib.rs:90-104. I verified it's inert without a localhost operator URL (a hostile node can't reach it, no key leaks), so this is fine as a default-off escape hatch. If you'd rather keep insecure code out of the shipped binary entirely, injecting the base URL/client at a test seam in gl would avoid the runtime flag.
  • [P3] crates/gitlawb-node/src/api/profiles.rs — the clippy borrow tweak is unrelated to this PR and is the same one-liner as #169; it'll no-op once either merges.

Core is solid. @kevincodex1 good to merge once you're happy with it.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found two issues that need to be addressed before this is ready.

Findings

  • [P2] Keep a loopback test origin from trusting a different loopback port
    crates/icaptcha-client/src/lib.rs:97
    With GITLAWB_ICAPTCHA_INSECURE=1, an operator URL of http://localhost:3000, and GITLAWB_ICAPTCHA_API_KEY configured, a hostile node can advertise x-icaptcha-url: http://localhost:9000. The new branch accepts both URLs, while resolve_solver_url allowlists and marks the selected endpoint key-trusted using only host_str; it therefore chooses :9000 and sends the operator's bearer key there. Compare normalized origins (including the effective port) before honoring an insecure advertised URL, or keep advertised HTTP URLs untrusted, and add the distinct-port regression case.

  • [P2] Assert that the retry uses the proof produced by the solver
    crates/gl/src/http.rs:503
    The success-path node mock accepts any x-icaptcha-proof, so this test remains green if send_signed substitutes a stale or unrelated nonempty token instead of the mock.proof returned by the iCaptcha answer mock. Match x-icaptcha-proof: mock.proof here so the new full-flow test makes the solve-to-retry value transfer load-bearing.

@kevincodex1

Copy link
Copy Markdown
Contributor

hello @Gravirei thanks for the contribution please address @jatmn comments

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

Labels

crate:gl gl — the contributor CLI crate:node gitlawb-node — the serving node and REST API kind:test Test coverage or harness

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Registration hard-blocked for new users: node v0.5.0 requires iCaptcha proof but released gl v0.3.8 predates icaptcha-client

5 participants