Skip to content

fix: validate the landing selection plan id before building URLs from it - #91

Merged
smarcet merged 9 commits into
masterfrom
fix/sp-landing-validation-cu-86b959jmg
Aug 13, 2026
Merged

fix: validate the landing selection plan id before building URLs from it#91
smarcet merged 9 commits into
masterfrom
fix/sp-landing-validation-cu-86b959jmg

Conversation

@smarcet

@smarcet smarcet commented Aug 11, 2026

Copy link
Copy Markdown

Ref.: https://app.clickup.com/t/86b959jmg

Alternative to #87, fixing the same redirect loop at its cause instead of at the routing layer.

Root cause

SP_LANDING is stored in localStorage globally, not per summit (src/app.js:113-117), so it can hold a selection plan id belonging to another summit or one the member is no longer allowed on. Three places built URLs from that value without validating it, and every one of them fed a route that SelectionPlanLayout's allowed-plan guard bounces back — which is what produced the infinite loop on /all-plans/{id}/profile.

Fix

One helper, getAllowedLandingSelectionPlanId(summit), validates the stored id against summit.selection_plans and returns null when the id is not among the member's allowed plans for this summit. The three consumers reuse it:

  • SummitLayout's no-speaker-profile redirect — falls back to /all-plans/profile
  • NavMenu links (My Bio / Tracks Guide / Selection Process) — fall back to the global routes
  • getSubmissionsPath() — falls back to all-plans

Why not #87's approach

That PR adds a dedicated route that bypasses SelectionPlanLayout entirely. That also skips getSelectionPlanSettings, which is the only thing populating selectionPlansSettings for a plan, so a show customizing CFP_SPEAKERS_SINGULAR_LABEL silently falls back to "Speaker" on a direct load. It also leaves two divergent route definitions for the same URL, and leaves the stale id driving the other menu links. Here allowed plans keep going through SelectionPlanLayout, so per-plan settings still load.

Also included

jest was in node_modules but not in package.json, and the jest/babel configs were untracked, so the suite could not run outside a dev machine. Pinned it, added yarn test, tracked both configs, and added a jest workflow mirroring summit-admin's.

Verification

9 unit tests on the helper and getSubmissionsPath, verified red-then-green. yarn build-dev compiles. Fresh-clone yarn install + yarn test both exit 0.

Not verified in a browser — that needs a summit with a user without a speaker profile and a stale SP_LANDING.

Summary by CodeRabbit

  • Bug Fixes

    • Improved navigation after saving profiles and completing presentations by validating the selected landing plan for the current summit.
    • Prevented redirect loops and provided a safe fallback when landing-plan settings are unavailable, invalid, or outdated.
  • Tests

    • Added automated coverage for valid, invalid, missing, malformed, and unavailable landing-plan scenarios.
  • Chores

    • Added automated test execution for pushes and pull requests.
    • Added project configuration to support running the automated test suite.

SP_LANDING is stored globally and can outlive the summit it was set on,
so SummitLayout could redirect to /all-plans/{id}/profile for a plan the
user cannot submit to. SelectionPlanLayout's guard then bounced back to
/all-plans, which redirected again -> infinite loop.

Validate the id against summit.selection_plans and fall back to the
global /all-plans/profile route when it is not submittable. Allowed plans
keep going through SelectionPlanLayout, so per-plan marketing settings
still load.
NavMenu read SP_LANDING directly, so a landing plan the user cannot
submit to produced My Bio / Tracks Guide / Selection Process links that
the allowed-plan guard bounced back to /all-plans. Reuse
getAllowedLandingSelectionPlanId so those fall back to the global routes,
and stop filtering the summit documents by an unreachable plan.
jest was present in node_modules but absent from package.json, and the
jest/babel configs were untracked, so `yarn test` did not exist and the
suite could not run in CI or on a fresh clone.

Pin jest, add the test script, and track both configs. babel.config.js
is scoped to env.test, so the webpack build (which configures babel-loader
with its own inline presets) is unaffected.
getSubmissionsPath read SP_LANDING directly, so the redirects after
saving a profile or finishing a presentation could point at
/all-plans/{id} for a plan the user cannot submit to, landing on the
allowed-plan guard. Take the summit and reuse
getAllowedLandingSelectionPlanId, falling back to the global path.
Mirrors the jest workflow already used in summit-admin, so the unit
tests added alongside the SP_LANDING fixes actually gate changes instead
of only running on a dev machine. Node 22 matches .nvmrc.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f9b853a3-14cf-43b9-bc9d-45fae700d174

📥 Commits

Reviewing files that changed from the base of the PR and between a14e934 and d31790e.

📒 Files selected for processing (2)
  • src/utils/methods.js
  • src/utils/methods.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/methods.test.js

📝 Walkthrough

Walkthrough

The PR validates stored landing selection plans against the current summit, updates summit-aware submission redirects and navigation, removes an unused import, and adds Jest configuration with GitHub Actions test execution.

Changes

Submission routing

Layer / File(s) Summary
Landing-plan validation and path generation
src/utils/methods.js, src/utils/methods.test.js
getAllowedLandingSelectionPlanId(summit) rejects invalid or unavailable stored plans. getSubmissionsPath(summit) uses the allowed plan or falls back to all-plans. Tests cover these cases.
Summit-aware routing integration
src/components/nav-menu/index.js, src/layouts/summit-layout.js, src/actions/speaker-actions.js, src/pages/preview-presentation-page.js, src/pages/thankyou-presentation-page.js, src/pages/profile-page.js
Routing and redirects pass summit context to the updated helpers. Profile redirects use the landing plan only when submission is allowed.
Jest test automation
.github/workflows/jest.yml, babel.config.js, jest.config.js, package.json
The project adds Jest configuration, Babel test presets, a test script, Jest 30.4.2, and a GitHub Actions workflow using Node.js 22.

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

Mergeability Score: ⚪ Minimal · up to d3179

The PR validates stored landing selections before building URLs and adds test tooling; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: santipalenque

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: validating the landing selection plan ID before constructing URLs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sp-landing-validation-cu-86b959jmg

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

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
.github/workflows/jest.yml (1)

14-14: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use an immutable dependency install.

If package.json and yarn.lock drift, plain yarn install can resolve or write dependency versions instead of failing. Use the lockfile enforcement flag for the declared Yarn version, such as --frozen-lockfile for Yarn Classic or --immutable for Yarn Berry.

🤖 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 @.github/workflows/jest.yml at line 14, Update the dependency installation
step in the Jest workflow from plain yarn install to the lockfile-enforcing
option supported by the repository’s declared Yarn version, using
--frozen-lockfile for Yarn Classic or --immutable for Yarn Berry.
jest.config.js (1)

1-4: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Add browser-level tests for redirect and navigation behavior.

testEnvironment: 'node' is sufficient for the only matched test file. That file mocks global.localStorage and uses no other browser globals. The application contains redirect and navigation paths, but no tests cover them.

🤖 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 `@jest.config.js` around lines 1 - 4, Add browser-level tests covering the
application’s redirect and navigation behavior, while preserving the existing
Jest configuration and its node test environment. Add the tests under the
existing src test-match pattern and mock only the browser globals required by
those paths, including localStorage where needed.
🤖 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 @.github/workflows/jest.yml:
- Around line 3-6: Add explicit read-only workflow permissions using contents:
read at the workflow or build job level near the existing on and jobs
configuration. Ensure the workflow does not inherit broader GITHUB_TOKEN
permissions.
- Around line 8-10: Update the actions/checkout step in the Jest workflow to set
persist-credentials to false, preserving fetch-depth: 0 and avoiding credential
persistence unless a later workflow step explicitly requires Git authentication.
- Around line 8-13: Update the actions/checkout and actions/setup-node uses in
the Jest workflow from v3 to the repository-approved supported major versions,
preserving their existing configuration; then rerun actionlint to verify the
workflow.

In `@src/utils/methods.js`:
- Around line 283-284: Update the selection plan ID parsing near the storedId
handling in src/utils/methods.js: replace parseInt validation with Number and
require a positive safe integer, or equivalent canonical positive-integer
validation, before matching a plan. Add tests in src/utils/methods.test.js lines
46-52 covering “123abc” and “1.5”, both expecting null.

---

Nitpick comments:
In @.github/workflows/jest.yml:
- Line 14: Update the dependency installation step in the Jest workflow from
plain yarn install to the lockfile-enforcing option supported by the
repository’s declared Yarn version, using --frozen-lockfile for Yarn Classic or
--immutable for Yarn Berry.

In `@jest.config.js`:
- Around line 1-4: Add browser-level tests covering the application’s redirect
and navigation behavior, while preserving the existing Jest configuration and
its node test environment. Add the tests under the existing src test-match
pattern and mock only the browser globals required by those paths, including
localStorage where needed.
🪄 Autofix

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: af1a8a25-3ad1-4d57-9323-f7501b9e42d9

📥 Commits

Reviewing files that changed from the base of the PR and between 7b4393f and a14e934.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • .github/workflows/jest.yml
  • babel.config.js
  • jest.config.js
  • package.json
  • src/actions/speaker-actions.js
  • src/components/nav-menu/index.js
  • src/layouts/summit-layout.js
  • src/pages/preview-presentation-page.js
  • src/pages/profile-page.js
  • src/pages/thankyou-presentation-page.js
  • src/utils/methods.js
  • src/utils/methods.test.js
💤 Files with no reviewable changes (1)
  • src/pages/profile-page.js

Comment thread .github/workflows/jest.yml
Comment thread .github/workflows/jest.yml
Comment thread .github/workflows/jest.yml
Comment thread src/utils/methods.js

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.

Pull request overview

This PR fixes an infinite redirect loop caused by using a globally-stored landing selection plan ID (SP_LANDING) without validating it against the currently-loaded summit’s allowed/submittable selection plans, and it also makes the repo’s unit test setup runnable in CI.

Changes:

  • Introduces getAllowedLandingSelectionPlanId(summit) and updates all URL-building consumers to fall back to safe “global” routes when the stored ID is stale/invalid.
  • Updates getSubmissionsPath to validate against the current summit before returning a plan-specific path, and adjusts call sites accordingly.
  • Adds Jest + Babel/Jest configs, a small unit test suite, and a GitHub Actions workflow to run tests.

Reviewed changes

Copilot reviewed 11 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/utils/methods.js Adds validation helper and makes getSubmissionsPath summit-aware to avoid stale-ID redirects.
src/utils/methods.test.js Adds unit tests for the new helper and the updated submissions path behavior.
src/layouts/summit-layout.js Uses validated landing plan ID to prevent redirect loops for users without a profile.
src/components/nav-menu/index.js Uses validated landing plan ID so menu links don’t send users into guarded routes that bounce back.
src/actions/speaker-actions.js Updates post-save redirect to use summit-aware getSubmissionsPath.
src/pages/thankyou-presentation-page.js Updates navigation to use summit-aware getSubmissionsPath.
src/pages/preview-presentation-page.js Updates navigation to use summit-aware getSubmissionsPath.
src/pages/profile-page.js Removes now-unused getSubmissionsPath import.
package.json Adds jest and yarn test script.
jest.config.js Adds Jest configuration for test discovery and environment.
babel.config.js Adds Babel config scoped to test environment for Jest transforms.
.github/workflows/jest.yml Adds CI workflow to run unit tests on pushes/PRs.
yarn.lock Locks new/updated dependencies for Jest and related tooling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/utils/methods.js
Comment thread .github/workflows/jest.yml
@smarcet smarcet self-assigned this Aug 11, 2026

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

Fix is at the right layer and the mechanism holds up. Verified independently:

  • The predicate matches SelectionPlanLayout's guard exactly (selection-plan-layout.js:30-32), which is why the loop closes.
  • Load ordering is safe. summit.selection_plans is replaced wholesale by /selection-plans/me (base-reducer.js:98), and SummitLayout returns null until dataLoaded (summit-layout.js:37), set only after getAllowedSelectionPlans resolves. NavMenu renders only under all-plans-layout, inside that gate. No consumer sees the pre-me list.
  • Not clearing an invalid SP_LANDING looks like an omission but is right: summit is undefined early, so remove on invalid would wipe a valid landing plan before the allowed list arrives.
  • Suite runs green at a14e934, 9/9, on a clean checkout with jest@30.4.2 plus babel-jest installed.

Four things outside the inline comments:

yarn.lock picked up an npm resolved tree. The branch adds 202 registry.npmjs.org entries against a lockfile that is otherwise 1001 registry.yarnpkg.com entries, and @babel/core, parser, traverse and types are each now resolved twice (7.29.0/7.29.1 and 7.29.7/7.29.8) instead of deduping onto versions that already satisfy jest's ^7.27.4. That is install and CI time for nothing. npx yarn-deduplicate && yarn install should collapse it.

nav-menu/index.js:38 still wraps parseInt(currentSP || landingSP). landingSP is a number now, so the wrapper is dead. Cosmetic.

Doc filtering in NavMenu changes as a side effect. With a stale landing id and no currentSP, otherDocs (nav-menu/index.js:34-53) used to filter to the stale plan's docs, likely showing none, and now falls through to showing every selection plan doc. Reads as an improvement, but it is the one user visible change nobody has looked at. Worth folding into the browser pass already flagged for the redirect itself.

Two workflow nits on top of what CodeRabbit and Copilot already flagged: cache: yarn on setup-node, and testMatch in jest.config.js will not pick up *.test.jsx when component tests arrive.

Comment thread src/utils/methods.js Outdated
Comment thread src/utils/methods.js Outdated
Comment thread src/utils/methods.js
Comment thread src/utils/methods.test.js
Its three callers were all converted to getAllowedLandingSelectionPlanId in
this branch; grep across src/ finds no remaining reference. Leaving the
unvalidated reader exported next to the validated one invites reintroducing
the redirect-loop bug this PR fixes.
The docstring said 'not submittable' / summit-layout's redirect comment said
'closed plan', but the check only verifies membership in
summit.selection_plans, which is not filtered by submission window
(summit-api OAuth2SummitSelectionPlansApiController::getMySelectionPlans
filters on summit_id, is_enabled and allowed_member_email only). A closed
plan the member is still allowed on correctly stays in the list -- speakers
need /all-plans/{id} to see what they submitted after the window shuts.

Reword to 'not among the member's allowed plans for this summit', and note
the predicate mirrors selection-plan-layout.js's guard on purpose: that
identity is what closes the redirect loop.
setLanding assigned global.localStorage and never restored it. Harmless
with a single test file today, but a second file assuming a clean global
would break depending on run order.
@smarcet
smarcet requested review from caseylocker and a lite review from Copilot August 13, 2026 17:32

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.

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

Thanks, the four items are addressed. One leftover on the reword: the same "cannot submit" framing is still in three other places, and 2e5d1d0's commit message says summit-layout's comment was reworded but that diff only touches src/utils/methods.js.

The contract is membership in summit.selection_plans, not submittability. /selection-plans/me filters on summit_id, is_enabled and allowed_member_email only, so a plan whose submission window has closed is still in that array and the helper deliberately keeps it. These three read the other way:

src/utils/methods.test.js:39

// stale SP_LANDING: id kept from another summit or a closed plan.

The second half names a case the helper accepts. Suggest: // stale SP_LANDING: id from another summit, or a plan no longer allowed for this member.

src/layouts/summit-layout.js:42

// only keep the landing plan in the URL if the user can actually submit to it, otherwise

Suggest: // only keep the landing plan in the URL if it is still one of the user's allowed plans, otherwise

src/components/nav-menu/index.js:27

// a landing plan the user cannot submit to would build menu links that the allowed-plan

Suggest: // a landing plan outside the user's allowed set would build menu links that the allowed-plan

Comments only, no behavior change. Rest of the PR looks good to me.

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

Once those outstanding "cannot submit" items are cleared up then it's all good.

@smarcet
smarcet merged commit 1ed5935 into master Aug 13, 2026
3 checks passed
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.

3 participants