diff --git a/CHANGELOG.md b/CHANGELOG.md index 735110f7f..1ecf465a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Fixed +- `WheelsTest` auto-bind missed user-defined global helpers added via `include` in `app/global/functions.cfm`. The pseudo-constructor used `getMetaData(application.wo).functions`, which only enumerates methods declared directly on the CFC and skips symbols merged in via `cfinclude`. Specs that called custom helpers (e.g. `can()`, `hasRole()`) had to manually rebind each one in `beforeAll()`. The auto-bind now iterates `application.wo` as a struct and binds every UDF via `isCustomFunction()`, preserving the existing public-only filter for declared methods (#2790) - Model layer SELECT clause builder now routes column identifiers through the adapter's `$quoteIdentifier`, so reserved-word column names (e.g. `key`, `order`, `group`) survive on every supported dialect instead of breaking `findAll` / `findOne` / dynamic finders with cryptic SQL syntax errors. The WHERE / ORDER BY paths already quoted columns; `$createSQLFieldList` and the empty-pagination column-list extraction in `read.cfc` now match. - `wheels packages install ` is now a transparent alias for `wheels packages add ` on every caller path that actually reaches `Module.cfc` (stdio MCP server, scripted in-process clients, spec suite). Previously the dispatch layer's `case "install":` branch printed a yellow warning to stdout and returned `""` without installing anything — so even though `PackagesMainCli.install()` itself had been a true alias for `add()` since #2729, any caller routing through the CLI module's verb dispatch silently no-op'd. The shell-facing `wheels packages install ` form is still intercepted by LuCLI's built-in extension installer upstream of module dispatch and remains broken on that path (and is documented as such in the module-owned help text), but MCP tool calls and programmatic callers now behave identically to `add`. Both branches now share a single fall-through body so the validation, error shape, and install behavior cannot drift apart again. - `wheels.wheelstest.BrowserTest` now throws a clear `Wheels.BrowserTest.NotWired` error — naming `browserDescribe()` as the fix — when a spec calls a DSL method on `this.browser` from a plain `describe()` block. Previously the uninitialized `this.browser` was an empty string, producing the misleading `function [visitUrl] does not exist in the String` on every newcomer's first BrowserTest spec. A sentinel `UnwiredBrowserGuard` is now installed at `this.browser` before `browserDescribe()` wires the real `BrowserClient` and after `$endBrowserContext()` tears it down diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 96cb87048..6b3a2b365 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -3852,4 +3852,27 @@ return local.$wheels; // User-defined global functions include "/app/global/functions.cfm"; + // Promote include-injected UDFs from `variables` to `this` so they're + // discoverable via struct-iteration on engines (Adobe CF) where only + // `this`-scope members are reliably enumerable. Declared methods on + // Global.cfc are already in `this` via their `access` modifier and are + // not clobbered by the `structKeyExists(this, ...)` guard. See #2790 + // and the auto-bind loop in `vendor/wheels/WheelsTest.cfc`. + // + // The leading `local.varKey = ""` seeds the `local` scope: Lucee 7's + // pseudo-constructor doesn't auto-create `local` for the iterator + // target of `for (local.X in Y)`, throwing "variable [local] doesn't + // exist" without a prior assignment. The same pattern is used in + // `WheelsTest.cfc` (which seeds `local.metaIndex = {}` before its loop). + local.varKey = ""; + for (local.varKey in variables) { + if (!isCustomFunction(variables[local.varKey])) { + continue; + } + if (structKeyExists(this, local.varKey)) { + continue; + } + this[local.varKey] = variables[local.varKey]; + } + } diff --git a/vendor/wheels/WheelsTest.cfc b/vendor/wheels/WheelsTest.cfc index 9b261ac67..b74cd9df5 100644 --- a/vendor/wheels/WheelsTest.cfc +++ b/vendor/wheels/WheelsTest.cfc @@ -10,18 +10,31 @@ component extends="wheels.wheelstest.system.BaseSpec" { // Pseudo-constructor (runs automatically) if (structKeyExists(application, "wo")) { - local.methods = getMetaData(application.wo).functions; - - for (local.method in local.methods) { - // Only add public, non-inherited methods - if (local.method.access eq "public") { - local.methodExists = structKeyExists(variables, local.method.name) || structKeyExists(this, local.method.name); + // Iterate struct keys on application.wo and bind every UDF. This + // catches both methods declared on Global.cfc (visible to + // getMetaData) AND helpers merged in via cfinclude (e.g. + // app/global/functions.cfm), which getMetaData(application.wo).functions + // does NOT enumerate — see #2790. + local.metaIndex = {}; + for (local.fn in getMetaData(application.wo).functions) { + local.metaIndex[local.fn.name] = local.fn.access; + } - if (!local.methodExists) { - variables[local.method.name] = application.wo[local.method.name]; - this[local.method.name] = application.wo[local.method.name]; - } + for (local.key in application.wo) { + if (!isCustomFunction(application.wo[local.key])) { + continue; + } + // For methods present in CFC metadata, keep the existing + // public-only filter; include-injected helpers have no + // access modifier so they're treated as public. + if (structKeyExists(local.metaIndex, local.key) && local.metaIndex[local.key] neq "public") { + continue; + } + if (structKeyExists(variables, local.key) || structKeyExists(this, local.key)) { + continue; } + variables[local.key] = application.wo[local.key]; + this[local.key] = application.wo[local.key]; } } diff --git a/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc b/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc new file mode 100644 index 000000000..8c4d42f7a --- /dev/null +++ b/vendor/wheels/tests/specs/wheelstest/WheelsTestAutoBindIncludesSpec.cfc @@ -0,0 +1,112 @@ +/** + * Regression: WheelsTest auto-bind misses include-injected helpers (#2790). + * + * `WheelsTest.cfc` uses `getMetaData(application.wo).functions` to discover + * which Wheels globals to bind into the spec's `variables` / `this` scopes. + * That metadata enumerates only methods defined directly on the CFC body, + * NOT symbols merged in via `cfinclude` / `include` (which is how + * `vendor/wheels/Global.cfc` pulls `/app/global/functions.cfm` at the bottom + * of the file). User-defined global helpers therefore worked in controllers / + * views / models but were invisible to test specs — every spec had to + * manually rebind helpers in `beforeAll()`. + * + * Cross-engine note: a bare `include` inside a CFC body lands UDFs in the + * component's `variables` scope, not `this`. Lucee's struct-iteration over + * a CFC instance surfaces both scopes, but Adobe CF's contract only + * reliably exposes `this`-scope members. To make the auto-bind path + * uniform across engines, `vendor/wheels/Global.cfc` promotes include- + * injected UDFs from `variables` to `this` immediately after the include + * runs. These specs simulate that post-promotion shape by assigning the + * probe UDF directly to `application.wo` (bracket-notation assignment + * from outside writes to `this`), then assert that: + * + * - The probe is invisible to `getMetaData(application.wo).functions` + * (the bug precondition the old code missed). + * - The probe is enumerated by `for (key in application.wo)` — the + * iteration mechanism the new auto-bind loop relies on. Failure here + * on any engine means the auto-bind loop will silently miss the helper. + * - The probe lands on a fresh `wheels.WheelsTest` instance and is + * callable. + */ +component extends="wheels.WheelsTest" { + + function run() { + + describe("WheelsTest auto-bind", () => { + + describe("helpers attached to application.wo outside of CFC metadata (issue ##2790)", () => { + + it("the bug precondition holds: include-style UDFs are invisible to getMetaData", () => { + var probeName = "$bot2790MetaProbe"; + application.wo[probeName] = function() { + return "metadata-probe"; + }; + try { + var meta = getMetaData(application.wo).functions; + var foundInMeta = false; + for (var fn in meta) { + if (fn.name == probeName) { + foundInMeta = true; + break; + } + } + expect(foundInMeta).toBeFalse(); + expect(structKeyExists(application.wo, probeName)).toBeTrue(); + expect(isCustomFunction(application.wo[probeName])).toBeTrue(); + } finally { + structDelete(application.wo, probeName); + } + }); + + it("for-in iteration over application.wo enumerates the probe key", () => { + // Guards the iteration mechanism the auto-bind loop in + // WheelsTest.cfc relies on. If this fails on any engine + // (notably Adobe CF, where struct-iteration over a CFC + // only reliably exposes this-scope members), the bind + // case below will silently pass-but-not-test. + var probeName = "$bot2790IterProbe"; + application.wo[probeName] = function() { + return "iter-probe"; + }; + try { + var seen = false; + for (var key in application.wo) { + if (key == probeName) { + seen = true; + break; + } + } + expect(seen).toBeTrue(); + } finally { + structDelete(application.wo, probeName); + } + }); + + it("auto-binds include-style helpers into a fresh WheelsTest instance", () => { + var probeName = "$bot2790BindProbe"; + application.wo[probeName] = function() { + return "bind-probe"; + }; + try { + var freshSpec = new wheels.WheelsTest(); + expect(structKeyExists(freshSpec, probeName)).toBeTrue(); + var bound = freshSpec[probeName]; + expect(bound()).toBe("bind-probe"); + } finally { + structDelete(application.wo, probeName); + } + }); + + it("still binds methods that ARE in CFC metadata (regression guard for the existing path)", () => { + var freshSpec = new wheels.WheelsTest(); + expect(structKeyExists(freshSpec, "model")).toBeTrue(); + expect(structKeyExists(freshSpec, "urlFor")).toBeTrue(); + }); + + }); + + }); + + } + +}