Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` is now a transparent alias for `wheels packages add <name>` 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 <name>` 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
Expand Down
23 changes: 23 additions & 0 deletions vendor/wheels/Global.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

}
33 changes: 23 additions & 10 deletions vendor/wheels/WheelsTest.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});

});

});

}

}
Loading