|
| 1 | +/** |
| 2 | + * Issue ##3311 — dev-mode inline notice when ?reload=true is refused. |
| 3 | + * |
| 4 | + * Since the fail-closed reload gate (##3062, 4.0.4), a refused ?reload=true is |
| 5 | + * a silent no-op in the browser: the only signals are wheels_security.log and |
| 6 | + * the boot warning, which keeps producing "reload is broken" support reports. |
| 7 | + * |
| 8 | + * The app template's reload gate now records WHY a requested reload did not |
| 9 | + * fire in request.wheels.reloadRefusedReason ("emptyPassword", |
| 10 | + * "missingPasswordParam", or the deliberately generic "refused" for |
| 11 | + * wrong-password/rate-limited), and the debug bar renders a banner for it — |
| 12 | + * in the development environment only. The "refused" reason must stay a |
| 13 | + * single generic message so the notice adds no oracle distinguishing a wrong |
| 14 | + * password from a rate-limited source. |
| 15 | + */ |
| 16 | +component extends="wheels.WheelsTest" { |
| 17 | + |
| 18 | + function run() { |
| 19 | + describe("debug.cfm reload-refused notice (issue 3311)", () => { |
| 20 | + |
| 21 | + it("renders the empty-password notice in development", () => { |
| 22 | + var output = $renderDebugBar(environment = "development", reason = "emptyPassword"); |
| 23 | + expect(output contains 'data-wdb-reload-refused="emptyPassword"').toBeTrue( |
| 24 | + "the banner must render and carry the emptyPassword reason" |
| 25 | + ); |
| 26 | + expect(output contains "<code>reloadPassword</code> is empty").toBeTrue( |
| 27 | + "the banner must name the setting that disables URL reload" |
| 28 | + ); |
| 29 | + expect(output contains "config/settings.cfm").toBeTrue( |
| 30 | + "the banner must say where to set the reload password" |
| 31 | + ); |
| 32 | + }); |
| 33 | + |
| 34 | + it("renders the missing-password-parameter notice in development", () => { |
| 35 | + var output = $renderDebugBar(environment = "development", reason = "missingPasswordParam"); |
| 36 | + expect(output contains 'data-wdb-reload-refused="missingPasswordParam"').toBeTrue( |
| 37 | + "the banner must render and carry the missingPasswordParam reason" |
| 38 | + ); |
| 39 | + expect(output contains "password parameter").toBeTrue( |
| 40 | + "the banner must say the password URL parameter is required" |
| 41 | + ); |
| 42 | + }); |
| 43 | + |
| 44 | + it("renders one generic notice for wrong-password/rate-limited refusals", () => { |
| 45 | + var output = $renderDebugBar(environment = "development", reason = "refused"); |
| 46 | + expect(output contains 'data-wdb-reload-refused="refused"').toBeTrue( |
| 47 | + "the banner must render and carry the generic refused reason" |
| 48 | + ); |
| 49 | + expect(output contains "wheels_security.log").toBeTrue( |
| 50 | + "the generic notice must point at wheels_security.log" |
| 51 | + ); |
| 52 | + // No oracle: the rendered notice must not say whether the password was |
| 53 | + // wrong or the source was rate-limited. |
| 54 | + expect(REFindNoCase("wrong|incorrect|rate.?limit", output) == 0).toBeTrue( |
| 55 | + "the generic notice must not distinguish wrong-password from rate-limited" |
| 56 | + ); |
| 57 | + }); |
| 58 | + |
| 59 | + it("renders no notice outside development even when a reason was recorded", () => { |
| 60 | + var output = $renderDebugBar(environment = "testing", reason = "emptyPassword"); |
| 61 | + expect(output contains "data-wdb-reload-refused").toBeFalse( |
| 62 | + "the notice is a development-only surface (issue 3311 acceptance criteria)" |
| 63 | + ); |
| 64 | + }); |
| 65 | + |
| 66 | + it("renders no notice when no refusal reason was recorded", () => { |
| 67 | + var output = $renderDebugBar(environment = "development", reason = ""); |
| 68 | + expect(output contains "data-wdb-reload-refused").toBeFalse( |
| 69 | + "no banner without a recorded refusal" |
| 70 | + ); |
| 71 | + }); |
| 72 | + |
| 73 | + }); |
| 74 | + } |
| 75 | + |
| 76 | + /** |
| 77 | + * Renders the debug bar template with the given environment and (optional) |
| 78 | + * request.wheels.reloadRefusedReason applied, restoring all touched state. |
| 79 | + * Modeled on DebugBarEnvQuickSwitchSpec.cfc. |
| 80 | + */ |
| 81 | + private string function $renderDebugBar(required string environment, required string reason) { |
| 82 | + var priorEnvironment = application.wheels.environment; |
| 83 | + var priorReqWheels = StructKeyExists(request, "wheels") ? Duplicate(request.wheels) : {}; |
| 84 | + // debug.cfm bails out (cfexit) when url.format is one of json/xml/csv/pdf |
| 85 | + // so it never breaks an API response. The test runner is hit with |
| 86 | + // format=json — clear it for the duration of the include. |
| 87 | + var hadUrlFormat = StructKeyExists(url, "format"); |
| 88 | + var priorUrlFormat = hadUrlFormat ? url.format : ""; |
| 89 | + var output = ""; |
| 90 | + try { |
| 91 | + application.wheels.environment = arguments.environment; |
| 92 | + if (!StructKeyExists(request, "wheels")) { |
| 93 | + request.wheels = {}; |
| 94 | + } |
| 95 | + request.wheels.execution = {total = 0}; |
| 96 | + request.wheels.params = {controller = "wheels", action = "tests", route = ""}; |
| 97 | + if (Len(arguments.reason)) { |
| 98 | + request.wheels.reloadRefusedReason = arguments.reason; |
| 99 | + } else { |
| 100 | + StructDelete(request.wheels, "reloadRefusedReason"); |
| 101 | + } |
| 102 | + if (hadUrlFormat) { |
| 103 | + StructDelete(url, "format"); |
| 104 | + } |
| 105 | + output = application.wo.$includeAndReturnOutput($template = "/wheels/events/onrequestend/debug.cfm"); |
| 106 | + } finally { |
| 107 | + application.wheels.environment = priorEnvironment; |
| 108 | + request.wheels = priorReqWheels; |
| 109 | + if (hadUrlFormat) { |
| 110 | + url.format = priorUrlFormat; |
| 111 | + } |
| 112 | + } |
| 113 | + return output; |
| 114 | + } |
| 115 | + |
| 116 | +} |
0 commit comments