@@ -11,6 +11,7 @@ import { isBillingLimitSettingsFormSubmission } from "~/routes/_app.orgs.$organi
1111import { getSuggestedRecoveryLimitDollars } from "~/components/billing/billingLimitFormat" ;
1212import {
1313 getAlertsResetRequested ,
14+ getBillingLimitReturnTo ,
1415 getEffectiveLimitCentsAfterLimitSave ,
1516 getResolveSubmitted ,
1617 getSubmittedResumeMode ,
@@ -164,6 +165,83 @@ describe("billingLimitsRoute.server", () => {
164165 expect ( getEffectiveLimitCentsAfterLimitSave ( "none" , 5000 ) ) . toBe ( 5000 ) ;
165166 } ) ;
166167 } ) ;
168+
169+ describe ( "getBillingLimitReturnTo" , ( ) => {
170+ const SETTINGS_PATH = "/orgs/acme/settings/billing-limits" ;
171+
172+ function formDataWithReturnTo ( returnTo ?: string ) : FormData {
173+ const formData = new FormData ( ) ;
174+ if ( returnTo !== undefined ) {
175+ formData . set ( "returnTo" , returnTo ) ;
176+ }
177+ return formData ;
178+ }
179+
180+ function returnToFor ( returnTo ?: string ) : string {
181+ return getBillingLimitReturnTo ( formDataWithReturnTo ( returnTo ) , "acme" ) ;
182+ }
183+
184+ it ( "returns an app-relative returnTo, including its query string" , ( ) => {
185+ expect ( returnToFor ( "/orgs/acme/projects/p/env/dev/runs?page=2" ) ) . toBe (
186+ "/orgs/acme/projects/p/env/dev/runs?page=2"
187+ ) ;
188+ } ) ;
189+
190+ it ( "keeps a dashboard page path with a multi-param query string" , ( ) => {
191+ expect ( returnToFor ( "/orgs/acme/projects/p/env/dev/runs?statuses=FAILED&page=3" ) ) . toBe (
192+ "/orgs/acme/projects/p/env/dev/runs?statuses=FAILED&page=3"
193+ ) ;
194+ } ) ;
195+
196+ it ( "falls back to the settings page when returnTo is missing" , ( ) => {
197+ expect ( returnToFor ( ) ) . toBe ( SETTINGS_PATH ) ;
198+ } ) ;
199+
200+ it ( "rejects protocol-relative URLs" , ( ) => {
201+ expect ( returnToFor ( "//evil.com" ) ) . toBe ( SETTINGS_PATH ) ;
202+ } ) ;
203+
204+ // WHATWG URL parsing treats a reverse solidus like a solidus in the authority position, so
205+ // each of these resolves to a foreign origin even though it opens with a single "/".
206+ it ( "rejects a reverse-solidus authority" , ( ) => {
207+ expect ( returnToFor ( "/\\evil.com" ) ) . toBe ( SETTINGS_PATH ) ;
208+ } ) ;
209+
210+ it ( "rejects a doubled reverse-solidus authority" , ( ) => {
211+ expect ( returnToFor ( "/\\\\evil.com" ) ) . toBe ( SETTINGS_PATH ) ;
212+ } ) ;
213+
214+ it ( "rejects a mixed solidus/reverse-solidus authority" , ( ) => {
215+ expect ( returnToFor ( "/\\/evil.com" ) ) . toBe ( SETTINGS_PATH ) ;
216+ } ) ;
217+
218+ it ( "rejects absolute URLs" , ( ) => {
219+ expect ( returnToFor ( "https://evil.com/runs" ) ) . toBe ( SETTINGS_PATH ) ;
220+ expect ( returnToFor ( "https://evil.example/runs" ) ) . toBe ( SETTINGS_PATH ) ;
221+ } ) ;
222+
223+ it ( "rejects resource routes" , ( ) => {
224+ expect ( returnToFor ( "/resources/preferences/favorites" ) ) . toBe ( SETTINGS_PATH ) ;
225+ } ) ;
226+
227+ it ( "rejects API routes" , ( ) => {
228+ expect ( returnToFor ( "/api/v1/tasks/my-task/trigger" ) ) . toBe ( SETTINGS_PATH ) ;
229+ } ) ;
230+
231+ it ( "rejects app pages outside the org section" , ( ) => {
232+ expect ( returnToFor ( "/account/security" ) ) . toBe ( SETTINGS_PATH ) ;
233+ } ) ;
234+
235+ it ( "rejects a bare /orgs path not nested under an organization" , ( ) => {
236+ expect ( returnToFor ( "/orgs" ) ) . toBe ( SETTINGS_PATH ) ;
237+ } ) ;
238+
239+ it ( "rejects a non-string returnTo" , ( ) => {
240+ const formData = new FormData ( ) ;
241+ formData . set ( "returnTo" , new Blob ( [ "/orgs/acme/runs" ] ) , "returnTo.txt" ) ;
242+ expect ( getBillingLimitReturnTo ( formData , "acme" ) ) . toBe ( SETTINGS_PATH ) ;
243+ } ) ;
244+ } ) ;
167245} ) ;
168246
169247describe ( "billing-alerts redirect route" , ( ) => {
@@ -217,6 +295,16 @@ describe("billing-limits form validation", () => {
217295 } ) ;
218296 } ) ;
219297
298+ it ( "accepts the org banner's no-limit field set" , ( ) => {
299+ const formData = new FormData ( ) ;
300+ formData . set ( "intent" , "billing-limit" ) ;
301+ formData . set ( "mode" , "none" ) ;
302+
303+ const submission = parseWithZod ( formData , { schema : billingLimitFormSchema } ) ;
304+ expect ( submission . status ) . toBe ( "success" ) ;
305+ expect ( submission . value ?. mode ) . toBe ( "none" ) ;
306+ } ) ;
307+
220308 it ( "parses none mode with cancelInProgressRuns from the form" , ( ) => {
221309 const formData = new FormData ( ) ;
222310 formData . set ( "mode" , "none" ) ;
0 commit comments