Sunday, 30 May 2021

CFML: weirdness with… <cfquery> and single-quote escaping

G'day:

I was looking around for a good (read: bad) example of one of those exercises in "building a dynamic SQL string inside a <cfquery>". Firstly, one should pretty much never do this. You are almost certainly tightly-coupling business logic with DB-tier logic, and that's an anti-pattern right there. And it also means you can't test it without hitting an actual database, which is rubbish. So just: don't do it. Build the SQL string first, testing all the logic as you go, and once the string is built, pass the completed thing to queryExecute (let's all just pretend <cfquery> doesn't exist: it's a horrific construct, and should have been left to die in the C20th where it belongs).

I found an excellent example of this in the CFWheels codebase.

<cfquery attributeCollection="#arguments.queryAttributes#">

    <cfset local.$wheels.pos = 0>

    <cfloop array="#arguments.sql#" index="local.$wheels.i">

        <cfset local.$wheels.pos = local.$wheels.pos + 1>

        <cfif IsStruct(local.$wheels.i)>
            <cfset local.$wheels.queryParamAttributes = $queryParams(local.$wheels.i)>

            <cfif
                NOT IsBinary(local.$wheels.i.value)
                AND local.$wheels.i.value IS "null"
                AND local.$wheels.pos GT 1
                AND (
                    Right(arguments.sql[local.$wheels.pos-1], 2) IS "IS"
                    OR
                    Right(arguments.sql[local.$wheels.pos-1], 6) IS "IS NOT"
                )
            >
                NULL
            <cfelseif StructKeyExists(local.$wheels.queryParamAttributes, "list")>
                <cfif arguments.parameterize>
                    (<cfqueryparam attributeCollection="#local.$wheels.queryParamAttributes#">)
                <cfelse>
                    (#PreserveSingleQuotes(local.$wheels.i.value)#)
                </cfif>
            <cfelse>
                <cfif arguments.parameterize>
                    <cfqueryparam attributeCollection="#local.$wheels.queryParamAttributes#">
                <cfelse>
                    #$quoteValue(str=local.$wheels.i.value, sqlType=local.$wheels.i.type)#
                </cfif> 
            </cfif>
        <cfelse>
            <cfset local.$wheels.i = Replace(PreserveSingleQuotes(local.$wheels.i), "[[comma]]", ",", "all")>
            #PreserveSingleQuotes(local.$wheels.i)#
        </cfif>
        #chr(13)##chr(10)#
    </cfloop>
    <cfif arguments.limit>
        LIMIT #arguments.limit#
        <cfif arguments.offset>
            #chr(13)##chr(10)#
            OFFSET #arguments.offset#
        </cfif>
    </cfif>
    <cfif Len(arguments.comment)>
        #arguments.comment#
    </cfif>
</cfquery>

(note that in the CFWheels codebase, that is all one single line of code, which I find to be… astonishing)

Also note that there are more "opportunities for improvement" there than just embedding very complicated string-building logic into a DB call. But it's a perfect example of what I'm talking about. And it's by no means the only example I've seen of this; it's all over the place in the CFML world. In its defence, at least the logic here is not really business logic; it's all SQL-string-building-logic. So it could be homed close to the DB call - maybe even a private method the same class - but it def doesn't belong inline like that.

Why? Because it's really complicated, ought to have been developed using TDD strategies, but to test this logic, one needs to actually hit a database, because all the logic is munged into the DB call! Ugh.

I thought to myself "but it's so easy to have built this in a purposeful string-builder method; I wonder why it wasn't done that way?". There's a coupla things that offer a challenge to this:

  • it's slinging the parameter values inline with <cfqueryparam> which is a bit "old skool", and also hasn't been necessary since CF11 (and its Railo / Lucee counterparts), provided one uses queryExecute which can correctly separate the SQL statement from the parameters for the statement.
  • It's got that preserveSingleQuotes in there.

preserveSingleQuotes is harder to deal with. Firstly a refresher. For some reason best known to someone at Allaire, all single quotes in any expressions within <cfquery> tags are automatically escaped. Whether appropriate to do so or not. To prevent this from happening, one needs to wrap the expression with a call to preserveSingleQuotes. But this only works if the call to preserveSingleQuotes is made within the <cfquery> tag. One cannot use preserveSingleQuotes outside that context (something not mentioned in any of the ColdFusion, Lucee or CFDocs, btw). This is such a daft thing for ColdFusion to have instigated. <cfquery> should just pass the string it's given to the DB. The end. If there's an expression within the SQL string that has single quotes that need to be escaped, then the code building the SQL string should do the escaping. Do it when it's needed. Don't do it unilaterally and then provide a mechanism to not do it. Blimey.

Anyway, that means that with all that string-building logic within that <cfquery>, it's not just a matter of lifting the string out into its own method, and building it in a (unit) testable fashion.

This got me looking at a way of stopping <cfquery> from escaping stuff, so that the preserveSingleQuotes call ain't needed.

Lucee provides a tag attribute on <cfquery> to disable it: psq, which if false, will not do the escaping. I do so wish they'd put more thought into the naming of that, but hey. Maybe escapeSingleQuotes or something. Not just a mostly meaningless TLA. Ah well: at least it's there and it works.

ColdFusion implements no such functionality, as far as I can tell.

During my googling as to how to address this, I came across one of my own old articles: "UnexpectedBuggy ColdFusion behaviour with CFML statements in <cfquery> tags". And within the grey box down at the bottom, there was mention of using UDFs within a <cfquery> tag varying the behaviour of quote-escaping. Then I thought… "hang on a sec…?" and tried an experiment.

Here's a baseline example showing the problem CFML causes for itself with quote escaping:

<cfscript>
numbers = queryNew("id,en,mi", "integer,varchar,varchar", [
    [1,"one","tahi"],
    [2,"two","rua"],
    [3,"three","toru"],
    [4,"four","wha"],
    [5,"five","rima"],
    [6,"six","ono"],
    [7,"seven","whitu"],
    [8,"eight","waru"],
    [9,"nine","iwa"],
    [10,"ten","tekau"]
])

sqlStatement = "
    SELECT *
    FROM numbers
    WHERE mi IN ('rua', 'toru', 'rima', 'whitu')
"
</cfscript>

<cftry>
    <cfquery name="primes" dbtype="query">#sqlStatement#</cfquery>
    <cfset dump =  primes>
    <cfcatch>
        <cfset dump = [cfcatch.message, cfcatch.detail, cfcatch.extendedinfo]>
    </cfcatch>
    <cffinally>
        <cfdump var="#dump#" label="no handling">
    </cffinally>
</cftry>

Here my SQL string has single quotes in it. It's supposed to have. They're legit. That's syntactically correct. But ColdFusion (and Lucee, but I'm showing this on ColdFusion) thinks it knows better, and messes it up:

It's "helpfully" escaped all the single quotes for me. Thus breaking the SQL statement. Cheers for that.

I can prevent this from happening by using preserveSingleQuotes:

<cfquery name="primes" dbtype="query">#preserveSingleQuotes(sqlStatement)#</cfquery>

Fine. And in reality, this could almost certainly be the way CFWheels handles the string building exercise. Build the string in a string builder method, then preserveSingleQuotes the whole lot.

But what's this thing about UDFs messing with stuff. This is the thing that kinda surprises me. Check this out:

<cfscript>
    function returnTheArgument(theArgument) {
        return theArgument
    }
</cfscript>

<!--- ... --->
<cfquery name="primes" dbtype="query">#returnTheArgument(sqlStatement)#</cfquery>
<!--- ... --->

This… works. My function doesn't do anything… but simply calling it on the SQL statement inline within the <cfquery> tags is enough to prevent the CFML engine from escaping any single quotes.

W … T.a. … F?

And this works on both CF and Lucee.


Anyway. That's "interesting". My conclusion as to how to solve the problem with the embedded preserveSingleQuotes thing is probably to just not embed it into the middle of things; just to wrap the whole statement with them. Which means all that nasty untestable logic can be lifted out of being inline there, and put in its own function that does the one thing. "Single Responsibility Principle" craziness, I know. But the road to this conclusion today was a winding one, that's for sure.

[mutters to self] I dunno what the CFML engines are thinking here. I really don't.

Righto.

--
Adam

Monday, 24 May 2021

Code smells: a look at a switch statement

G'day:

There was a section in last week's Working Code Podcast: Book Club #1 Clean Code by "Uncle Bob" Martin (pt2) where the team were discussing switch statements being a code smell to avoid in OOP (this is at about the 28min mark; I can't find an audio stream of it that I can deep-link to though). I didn't think they quite nailed their understanding of it (sorry team, I don't mean that to sound patronising), so afterwards I asked Namesake if it might be useful if I wrote an article on switch as a code smell. He confirmed that it might've been more a case of mis-articulation than not getting it, but I ought to go ahead anyhow. So I decided to give it some thought.

Coincidentally, I happened to be looking at some of Adam's own code in his Semaphore project, and something I was looking at the test for was… a switch statement. So I decided to think about that.

I stress I said I'd think about it because I'm def on the learning curve with all this stuff, and whilst I've seen some really smell switch statements, and they're obvious, I can't say that I can reason through a good solution to every switch I see. This is an exercise in learning and thinking for me.

Here's the method with the switch in it:

private boolean function ruleMathIsTrue(required any userAttributeValue, required string operator, required any ruleValue){
    switch (arguments.operator){
        case '=':
        case '==':
            return arguments.userAttributeValue == arguments.ruleValue;
        case '!=':
            return arguments.userAttributeValue != arguments.ruleValue;
        case '<':
            return arguments.userAttributeValue < arguments.ruleValue;
        case '<=':
            return arguments.userAttributeValue <= arguments.ruleValue;
        case '>':
            return arguments.userAttributeValue > arguments.ruleValue;
        case '>=':
            return arguments.userAttributeValue >= arguments.ruleValue;
        case 'in':
            return arrayFindNoCase(arguments.ruleValue, arguments.userAttributeValue) != 0;
        default:
            return false;
    }
}

First up: this is not an egregious case at all. It's isolated in a private method rather than being dumped in the middle of some other logic, and that's excellent. The method is close enough to passing a check of the single-responsibility principle to me: it does combine both "which approach to take" with "and actually doing it", but it's a single - simple - expression each time, so that's cool.

What sticks out to me though is the repetition between the cases and the implementation:

They're mostly the same except the three edge-cases:

  • = needs to map to ==;
  • in, which needs a completely different sort of operation.
  • Instead of just throwing an exception if an unsupported operator is used, it just goes "aah… let's just be false" (and return false and throwing an exception are both equally edge-cases anyhow).

This makes me itchy.

One thing I will say for Adam's code, and that helps me in this refactoring exercise, is that he's got good testing of this method, so I am safe to refactor stuff, and when the tests pass I know I'm all good.


My first attempt at refactoring this takes the approach that a switch can often be re-implemented as a map: each case is a key; and the payload of the case is just some handler. This kinda makes the method into a factory method (kinda):

operationMap = {
    '=' : () => userAttributeValue == ruleValue,
    '==' : () => userAttributeValue == ruleValue,
    '!=' : () => userAttributeValue != ruleValue,
    '<' : () => userAttributeValue < ruleValue,
    '<=' : () => userAttributeValue <= ruleValue,
    '>' : () => userAttributeValue > ruleValue,
    '>=' : () => userAttributeValue >= ruleValue,
    'in' : () => ruleValue.findNoCase(userAttributeValue) != 0
};
return operationMap.keyExists(operator) ? operationMap[operator]() : false

OK so I have a map - lovely - but it's still got the duplication in it, and it might be slightly clever, but it's not really as clear as the switch.


Next I try to get rid of the duplication by dealing with each actual case in a specific way:

operator = operator == "=" ? "==" : operator;
supportedComparisonOperators = ["==","!=","<","<=",">",">="];
if (supportedComparisonOperators.find(operator)) {
    return evaluate("arguments.userAttributeValue #operator# arguments.ruleValue");
}
if (operator == "in") {
    return arrayFindNoCase(arguments.ruleValue, arguments.userAttributeValue);
}
return false;

This works, and gets rid of the duplication, but it's way less clear than the switch. And I was laughing at myself by the time I wrote this:

operator = operator == "=" ? "==" : operator

I realised I could get rid of most of the duplication even in the switch statement:

switch (arguments.operator){
    case "=":
        operator = "=="
    case '==':
    case '!=':
    case '<':
    case '<=':
    case '>':
    case '>=':
        return evaluate("arguments.userAttributeValue #operator# arguments.ruleValue");
    case 'in':
        return arrayFindNoCase(arguments.ruleValue, arguments.userAttributeValue) != 0;
    default:
        return false;
}

Plus I give myself bonus points for using evaluate in a non-rubbish situation. It's still a switch though, innit?


The last option I tried was a more actual polymorphic approach, but because I'm being lazy and CBA refactoring Adam's code to inject dependencies, and separate-out the factory from the implementations, it's not as nicely "single responsibility principle" as I'd like. Adam's method becomes this:

private boolean function ruleMathIsTrue(required any userAttributeValue, required string operator, required any ruleValue){
    return new BinaryOperatorComparisonEvaluator().evaluate(userAttributeValue, operator, ruleValue)
}

I've taken the responsibility for how to deal with the operators out of the FlagService class, and put it into its own class. All Adam's class needs to do now is to inject something that implements the equivalent of this BinaryOperatorComparisonEvaluator.evaluate interface, and stop caring about how to deal with it. Just ask it to deal with it.

The implementation of BinaryOperatorComparisonEvaluator is a hybrid of what we had earlier:

component {

    handlerMap = {
        '=' : (operand1, operand2) => compareUsingOperator(operand1, operand2, "=="),
        '==' : compareUsingOperator,
        '!=' : compareUsingOperator,
        '<' : compareUsingOperator,
        '<=' : compareUsingOperator,
        '>' : compareUsingOperator,
        '>=' : compareUsingOperator,
        'in' : inArray
    }

    function evaluate(operand1, operator, operand2) {
        return handlerMap.keyExists(operator) ? handlerMap[operator](operand1, operand2, operator) : false
    }

    private function compareUsingOperator(operand1, operand2, operator) {
        return evaluate("operand1 #operator# operand2")
    }

    private function inArray(operand1, operand2) {
        return operand2.findNoCase(operand1) > 0
    }
}

In a true polymorphic handling of this, instead of just mapping methods, the factory method / map would just give FlagService the correct object it needs to deal with the operator. But for the purposes of this exercise (and expedience), I'm hiding that away in the implementation of BinaryOperatorComparisonEvaluator itself. Just imagine compareUsingOperator and inArray are instances of specific classes, and you'll get the polymorphic idea. Even having the switch in here would be fine, because a factory method is one of the places where I think a switch is kinda legit.

One thing I do like about this handling is the "partial application" approach I'm taking to solve the = edge-case.

But do you know what? It's still not as clear as Adam's original switch. What I have enjoyed about this exercise is trying various different approaches to removing the smell, and all the things I tried had smells of their own, or - in the case of the last one - perhaps less smell, but the code just isn't as clear.


I'm hoping someone reading this goes "ah now, all you need to do is [this]" and comes up with a slicker solution.


I'm still going to look out for a different example of switch as a code smell. One of those situations where the switch is embedded in the middle of a block of code that then goes on to use the differing data each case prepares, and the code in each case being non-trivial. The extraction of those cases into separate methods in separate classes that all fulfil a relevant interface will make it clearer when to treat a switch as a smell, and solve it using polymorphism.

I think what we take from this is the knowledge that one ought not be too dogmatic about stamping out "smells" just cos some book says to. Definitely try the exercise (and definitely use TDD to write the first pass of your code so you can safely experiment with refactoring!), but if the end result ticks boxes for being "more pure", but it's at the same time less clear: know when to back out, and just run with the original. Minimum you'll be a better programmer for having taken yerself through the exercise.

Thanks to the Working Code Podcast crew for inspiring me to look at this, and particularly to Adam for letting me use his code as a discussion point.

Righto.

--
Adam

Tuesday, 18 May 2021

I play the game "how long will it take me to find a new bug in ColdFusion" again, after a few years

G'day:

Answer: about two minutes to guess what would likely be broken, and I think I had an initial repro created in another minute after that.

Last time I tried this it took 44min: "ColdFusion bug challenge: how quickly can one find a bug in ColdFusion 11?".

This time I figured "ah what's a language feature in CF2021? I reckon anything more than superficial use of one of those will break. Um… the rest operator. I bet it can't handle type-checking properly". Actually that didn't even take two minutes to type, but it was about how long I thought about it. So: given I was actually right about that guess, it took me - what - 20seconds? I'm not claiming to be smart or anything here. It's just so frickin easy to find things wrong with CFML that that is as long as it might take.

For context, here's the rest operator in action in a simple situation:

function untypedVariadicArguments(string s, ...x) {
    return arguments
}
result = untypedVariadicArguments("some string", 1, "two", ["three"])

writeDump(result)

The rest operator enables one to define variadic functions: ones that can take any number of arguments. Of course CFML already supported this: you can pass whatever you like to a UDF / method, and all the values will be there in the arguments scope, whether you specify them in the function signature or not. The difference is that in a variadic function, all the trailing arguments are put in the same one parameter value as an array. The output of above is:

 


 That is working properly.

The bug comes in when I try to type-check the that x parameter, eg: it can have any number of x values but they all need to be numbers. I would expect the syntax to be like this:

function typedVariadicArguments(string s, numeric ...x) {
    return arguments
}

In most curly-brace languages I can find that do type-checked variadic functions, the type declaration is done before the parameter declaration (irrespective of whether the parameter syntax is ...paramName or paramName..., as that can vary). See the examples on that page on Wikipedia I link to above on variadic functions. If I run that code in ColdFusion though, I get this:

Invalid CFML construct found on line 2 at column 52. on line 2

(Ha, by the by I also just found a wee bug in Lucee. Lucee does not support the rest operation, and when I accidentally tried to run this on Lucee cos I was not paying attention to what I was doing on trycf.com, it errored with "invalid argument definition on line 2": it's a parameter not an argument. The parameter is the definition. The argument is the value you pass)

So I went "OK well there's a good chance they just did it wrong with ColdFusion", so I tried it like this:

function typedVariadicArguments(string s, ...numeric x) {
    return arguments
}

Now that at least compiled, however it seems to be completely ignoring the rest operator. I'm testing it with the same test code as above:

result = typedVariadicArguments("some string", 1, "two", ["three"])

writeDump(result)

This is how one would expect it to work with just a signature like this:

function typedVariadicArguments(string s, numeric x) {
    return arguments
}

ColdFusion really ought not be going "buggered if I know what those dots are doing there: I'll just ignore them". Note that is understanding that as the rest operator. If you try two dots or four dots, you'll get a compile error.

I'm going with two bugs here, actually:

  • An implementation omission in that numeric ...x should work. That's the syntax they should have used, and it's not valid to not implement the type check there.
  • ColdFusion should not be ignoring th rest operator in a method signature if it doesn't know what to do with it. It should error.
  • OK a third bug I think. It'll allow the rest operator on the non-last parameter without erroring (untypedVariadicArguments(...s, x)): it doesn't work, but again it just ignores it.

I'm gonna stop looking now.

Am I missing something? There are all bugs right? I'm not getting something wrong?

Righto.

--
Adam

Sunday, 16 May 2021

CFWheels: running TestBox instead of RocketUnit

G'day:

CFWheels ships with its own inbuilt (albeit third-party) testing framework. I discuss its merits in an earlier article: "Testing: A Horror Story". You can probably work out my opinion of the inbuilt testing framework - RocketUnit - from the title. That's really all you need to know to contextualise why I am now going to get TestBox working in a CFWheels context. One would expect that this would simply be a matter of installing TestBox and then using the CFWheels API to call methods on its classes to… um… use it. Not so fast there chief.

The CFWheels application has been implemented via a few opaque (to me) architectural decisions, which means it's not straight-forward to run the code outside the context of an application solely designed to respond to HTTP request in an MVC fashion. You've read along already perhaps with my experiences of hacking it about to even be installed in a sensible place ("Short version: getting CFWheels working outside the context of a web-browsable directory"). Code-wise, CFWheels is not an object-oriented application, instead being a more procedural affair, relying on include to share files full of functions. Lack of OO design means it's difficult to interact with in an isolated fashion that one might expect: leveraging/relying on encapsulation, abstraction, inheritance and various other programming techniques familiar to modern CFML. There's no API really, there's just a bunch of independent functions that are simultaneously disconnected from each other, but at the same time strongly-coupled: using a function homed in one .cfm file might require a function homed in another .cfm file, and one just has to know that. All functions are public (given they're not in CFCs, this is can't be helped), but not all of them are designed to be used outside of the CFWheels app (they're prefixed with $). Also the functions might not necessarily keep their variables encapsulated within their own scope; and also fairly frequently rely on other functions having done the same: exposing data into the calling context for it to be there for another funtion to use. There's no CFCs to instantiate, one just needs to include one or more of these .cfm files full of functions. This makes for a fairly fragile and confusing coding experience, IMO.

I'm not going to be daunted by this. I'm also not going to let this architectural approach bleed into my own code: I'm not writing procedural code when I'm using an OO language in 2021. Somehow I'm gonna wrap it up in an adapter component that my tests can leverage to test my code.

Controllers

My first task is to work out how to execute the code for a root programmatically. I could simply make an external HTTP request to to the route and inspect its response, but this approach won't help me with lower level testing (like calling model functionality, in the next step). So I'm going to work out how I can write code along these lines: myResponseObject = cfwheelsAdapter.someMethod(), so I can then poke and prod the response object to see if it's done the expected thing.

Looking through the CFWheels docs ("Testing Your Application"), I find this test example:

function testRedirectAndFlashStatus() {
  // define the controller, action and user params we are testing
  local.params = {
    controller="users",
    action="create",
    user={
      firstName="Hugh",
      lastName="Dunnit",
      email="hugh@somedomain.com",
      username="myusername",
      password="foobar",
      passwordConfirmation="foobar"
    }
    };

  // process the create action of the controller
  result = processRequest(params=local.params, method="post", returnAs="struct");
  
  // make sure that the expected redirect happened
  assert("result.status eq 302");
  assert("result.flash.success eq 'Hugh was created'");
  assert("result.redirect eq '/users/show/1'");
  
}

OK so I don't use a route, I just call the controller. That's cool, although I'd rather be doing this by providing a route, a method, and the various params the controller needs - query params, body content, headers, etc - and make like a request, but I might look into that later. For now the object of the exercise is to be able to call the CFWheels code and check the response. And the function I want to call is this processRequest. This is an example of what I was saying before: it's not a method of a controller object or anything, it's just a headless function. And looking at that code I have no idea where it's implemented or how it came to be available to call in my test method. CFWheels just assumes any code you write will already be in the right context for this to work. I'm going to create my adapter object and include whatever file that processRequest function is in.

Well. Actually first I'm gonna create a test for this operation (/test/functional/WheelsRequestRunnerTest.cfc):

component extends=testbox.system.BaseSpec {

    function run() {
        describe("Test WheelsRequestRunner functionality", () => {
            it("can execute a request and receive the expected response", () => {
                runner = new test.WheelsRequestRunner()

                result = runner.processRequest(
                    params = {
                        controller = "testRoute",
                        action = "httpStatusOkTest"
                    },
                    method = "get",
                    returnAs = "struct"
                )

                expect(result).toHaveKey("status")
                expect(result.status).toBe(200)
                expect(result).toHaveKey("body")
                expect(result.body).toInclude("[200-OK]")
            })
        })
    }
}

This is kinda eating its own dogfood. My test of the runner class is that it can be used to test a controller. The controller in question simply renders a view (indeed: there's no controller, it's just the view in this case), and the view does nothing other than emit [200-OK].

The WheelsRequestRunner class is gonna encapsulate any CFWheels files I need to include to make stuff work (and contain an variables-scope bleeding the include files leak). First up I need to expose this processRequest function, and before I do that, I need to find where CFWheels implements it. The only way to do this is just doing a find in the codebase for function processRequest. It turns out it's in /wheels/global/misc.cfm. Not a hugely helpful filename that, and I wonder why a very controller-specific function is in a file called global/misc: it'd be nice if a controller function was in a file called controller.cfm or something. Anyway, I sling misc.cfm into my runner class:

component {
    include template="/wheels/global/misc.cfm"
}

When I run my test now, I get an error:

No matching function [$ARGS] found
global.misc_cfm$cf.udfCall2(/wheels/global/misc.cfm:284)

I track-down $args (not an excellent fuction name: what is "$args" supposed to do?) in /wheels/global/internal.cfm, so I include that too.

component {
    include template="/wheels/global/misc.cfm"
    include template="/wheels/global/internal.cfm"
}

At this point my WheelsRequestRunner is exposing 72 public methods, when in reality I only need one of those. Less than ideal.

But we're not done: we have a second error:

No matching function [$GET] found
global.misc_cfm$cf.udfCall2(/wheels/global/misc.cfm:307)

I found $get in /wheels/global/util.cfm. I wonder what the perceived difference is between global/misc, and global/util? All three components of those file paths are just meaningless and geenric filler terms. But anyway… I'll include this too, and get another error, suggesting probably a fourth file I'm going to need to include:

No matching function [$DOUBLECHECKEDLOCK] found
global.misc_cfm$cf.udfCall2(/wheels/global/misc.cfm:482)

I found $doubleCheckedLock in /wheels/global/cfml.cfm. I'll include this too, and…

 

It works!

Note: WheelsRequestRunner has needed to expose 99 functions, spread across four disparate files, to avail itself of the one function it needed to call:

component {
    include template="/wheels/global/misc.cfm"
    include template="/wheels/global/internal.cfm"
    include template="/wheels/global/util.cfm"
    include template="/wheels/global/cfml.cfm"
}

I just want to do another controller test as well: an unhappy-path one:

it("can test a 400 response", () => {
    try{
        result = runner.processRequest(
            name = "testRoute",
            params = {
                controller = "testRoute",
                action = "badRequest"
            },
            method = "get",
            returnAs = "struct"
        )

        expect(result).toHaveKey("status")
        expect(result.status).toBe(400)
        expect(result).toHaveKey("body")
        expect(result.body).toInclude("[400-BAD-REQUEST]")
    } finally {
        header statuscode=200;
    }
})

This worked too without any more adjustment to WheelsRequestRunner. I've needed to put that try/finally around the test code because of the way CFWheels handles responses. It doesn't create a response object or anything like that that I could set my response status code in, the CFWheels approach is just that if I want to change the response status, I just change it … directly changing the header of the current request (in this case the request being made by the test runner). This is less than ideal. I'm currently using CFML's inbuilt header statement to do this, later when I work out how to do dependency injection with CFWheels I'll write my own wrapper to do this, and mock it out for the test. I do need to actively address this in my test though, because returning a 400 also means the test run reports as a failure because it doesn't complete with a 200-OK. Easily fixed, but I really oughtn't have to do this sort of thing.

For now, I'm satisfied I can test basic controllers. I want to move on to test a basic model, because I expect I'll need to do some more reconfiguration to make that work.

Models

I've created a dead-simple model class:

component extends=models.Model {

    function init()
    {
        table(false)
        property(name="name", label="Name")
        property(name="email", label="Email address")
        property(name="password", label="Password")
    }
}

I'm just going to check that I can create an instance of one of these, and the property values get used:

it("can test a model", () => {
    properties = {name="TEST_NAME", email="TEST_EMAIL", password="TEST_PASSWORD"}

    model = runner.model("SomeModel")
    testModel = model.new(properties)
    expect(testModel).toBeInstanceOf("models.SomeModel")
    expect(testModel.properties()).toBe(properties)
})

When I run this, I get another instance of CFWheels not being able to find something:

invalid component definition, can't find component [...src.models.SomeModel]

This is a different situation though: this is because - I am betting - because I am running the request from the /test/ directory, not the site's root. I'm going to need to dig around to see how CFWheels concludes that it should be looking for SomeModel in ...src.models.SomeModel. To misquote Captain Oates: "I am just going [into the CFWheels codebase] and may be some time."…

Right so I was close, but it was more my fault than anything. Kinda. Firstly I had a setting slightly wrong in my config/settings.cfm, due to a misunderstanding as to what CFWheels was expecting with some settings. Previously when configuring my CFWheels site to work with the source code "above" the web root, I had to make a coupla settings changes for CFWheels to be able to find my controllers and views, and those settings took file system paths:

set(viewPath = "../src/views")
set(controllerPath = "/app/controllers")

Without thinking I had just assumed that the equivalently-named modelPath would also be a file-system path, and had set it thus:

set(modelPath = "../src/models")

However it's actually a component path reference, so just needs to be this:

set(modelPath = "/models")

And this is used in conjunction with a mapping I had missing in Application.cfc:

component {

    // ...    

    thisDirectory = getDirectoryFromPath(getCurrentTemplatePath())
    // ... 
    this.mappings["/models"] = getCanonicalPath("#thisDirectory#models")
    // ... 
}

If I didn't have both of those, CFWheels was resolving the CFC path to be ...src.models.SomeModel, and even with that fixed, it was looking for the correct path (models.SomeModel) from a base context of the public directory, not the src directory. Once I sorted that out, the test passed.

Views

I wasn't actively going to test this because I've been wittering on enough already in this article, but last week I'd actually already written some tests that checked how the linkTo function worked under different URLRewriting settings, and I've just noticed the one test I had that was working satisfactorily regarding this is currently broken, and it looks to be down to what I've been doing with TestBox recently. Here's the test:

it("uses the URI when URLRewriting is on", () => {
    runner = new test.WheelsRequestRunner()
    runner.set(URLRewriting="On")

    result = runner.processRequest(
        params = {
            controller = "testRoute",
            action = "linktotest"
        },
        method = "get",
        returnAs = "struct"
    )

    expect(result).toHaveKey("body")
    expect(result.body).toMatch('<a\b[^>]*href="/testRoute/linkToTestTarget"[^>]*>')
})

It's pretty simple: it hits an endpoint that returns mark-up with a link generated by linkTo:

<cfoutput>#linkTo(route="testRouteLinkToTestTarget",text="Click Me")#</cfoutput>

This test had been working, but it's erroring with because the URL for the link it's returning is runTests.cfm/testroute/linktotesttarget. it should be just /testRoute/linkToTestTarget. "Coincidentally" the name of the file I'm running the tests from is… runTest.cfm. Hazarding a guess, I am assuming the linkTo is doing some shenanigans with a find and replace expecting the CGI.script_name to be just /index.cfm. The script name shouldn't come into it with a fully re-written URL, given it doesn't figure in it, but… well…. I'll track down the code in question…

… yeah it was as I suspected. In /wheels/global/misc.cfm we have this:

// When URL rewriting is on we remove the rewrite file name (e.g. rewrite.cfm) from the URL so it doesn't show.
// Also get rid of the double "/" that this removal typically causes.
if (arguments.$URLRewriting == "On") {
    local.rv = Replace(local.rv, application.wheels.rewriteFile, "");
    local.rv = Replace(local.rv, "//", "/");
}

(For future reference, CFWheels Team, if you feel the need to block-out code like that with an explanatory comment, it means a) your function is doing too much (this function is close to 200 lines long!!!); b) the code that's been blocked-out should be in its own function)

So yeah, CFWheels expecting one of two possibilities for the file part of the script name here: index.cfm or rewrite.cfm. My runTests.cfm doesn't cut the mustard. However this is easy enough to solve, I can override the setting in test/Application.cfc:

component extends=cfmlInDocker.Application {

    this.name = "testApplication"

    // ...

    function onApplicationStart() {
        super.onApplicationStart()
        set(rewriteFile="runTests.cfm")
    }

    // ...

}

I've also given the test app a new name so its application scope settings don't interfere with the application scope for the front of the site.

Summary

To get at least controllers and models testable wasn't much effort. I had to do this:

That's not so bad. It was more hassle finding out where the relevant CFWheels functions were, and following the logic to see what I had to override/fix and why, but the end result was all right. I'm just glad I can write decent tests now.

Righto.

--
Adam

Sunday, 9 May 2021

Setting up a MariaDB JDBC datasource in ColdFusion 2021 in Application.cfc

G'day:

This is how I needed to set my datasource in Application.cfc to get ColdFusion 2021 to connect to MariaDB. I'm writing this because I could not - for the life of me - find this information in any one place in the docs. Nor could I find it anywhere else via Google. I have pieced this together from various sources (including Lucee docs, which were more helpful than the Adobe ColdFusion documentation), and want to put it in one place for my own future reference, or should anyone else need to know how to set up a MariaDB datasouce in ColdFusion using their JDBC driver. The params should also work for any other JDBC datasource.

Apologies for that fairly SEO-heavy opening paragraph.

OK, so it's this:

component {

    this.datasources["test"] = {
        host = "localhost",
        port = 3306,
        driver = "mariadb",
        class = "org.mariadb.jdbc.Driver",
        url = "jdbc:mariadb://localhost:3306/database_name?user=user_name&password=user_password",
        database = "database_name",
        username = "user_name",
        password = "user_password", 
        custom = {
            useUnicode = true,
            characterEncoding = "UTF-8"
        }
    }
    this.datasource = "test"
}

After checking up on what Sean says in the comment below, I was able to pare this back to:

this.datasources["test"] = {
    class = "org.mariadb.jdbc.Driver",
    url = "jdbc:mariadb://localhost:3306/cfmlindocker?useUnicode=true&characterEncoding=UTF-8",
    username = "cfmlindocker",
    password = server.system.environment.MYSQL_PASSWORD
}

It failed if I did not have the username and password keys; but I did not need them in the URL. And I can pass the custom stuff in the URL, rather than specifying them separately.

Cheers for encouraging me to investigate further, Sean!

I got the MariaDB jar file from Download and Install MariaDB Connector/J. I also found the class name for the URL in there. That jar should be dropped into [coldfusion directory]/]WEB-INF/lib

The driver value seems to just be a label.

Note that on Lucee they seem to use connectionString rather than url. I dunno if url also works, and CBA checking right now.

ColdFusion might give an unhelpful error if it does't like the datasource settings. Instead of saying "I don't like those datasource settings", or "they don't work", it says "Datasource test could not be found". It was easy to find, pal: it's right there in the Application.cfc file. I just had a param wrong. I especially saw this when I had connectionString rather then url.

I would normally store the password in the environment, not hard-code into a source-controlled file, eg: server.system.environment.TEST_DSN_PASSWORD (both ColdFusion and Lucee put any environment variables in server.system.environment. Or you could use an environment-specific non-source controlled config file or something. Just don't put it directly in Application.cfc

Here's some more SEO for you: so that was the Application.cfc configuration settings for ColdFusion 2021 to connect to a MariaDB database using their JDBC connection.

Righto.

--
Adam

Thursday, 6 May 2021

Lucee: what now for goodness sake

G'day:

I'm just writing this here because it's too long to put in a message in the CFML/Lucee Slack channel, and so I can get some eyes on it.

Consider this code (test.cfm):

<cfoutput>
<cfset relativeFilePath = "../getCanonicalPathIssue/targetfile.cfm">
Relative file path: [#relativeFilePath#]<br>
Does relative path exist: [#fileExists(relativeFilePath)#]<br>
<br>

<cfset expandedPath = expandPath(relativeFilePath)>
Expanded file path: [#expandedPath#]<br>
Does expanded path exist: [#fileExists(expandedPath)#]<br>
<br>

<cfset canonicalPathFromRelativeFilePath = getCanonicalPath(relativeFilePath)>
Canonical path from relative path: [#canonicalPathFromRelativeFilePath#]<br>
Does canonical pathh from relative path exist: [#fileExists(canonicalPathFromRelativeFilePath)#]<br>
<br>

<cfset canonicalPathFromExpandedPath = getCanonicalPath(expandedPath)>
Canonical path from expanded path: [#canonicalPathFromExpandedPath#]<br>
Does canonical path from expanded path exist: [#fileExists(canonicalPathFromExpandedPath)#]<br>
<br>

<cfset directory = getDirectoryFromPath(canonicalPathFromRelativeFilePath)>
Directory: #directory#<br>
Directory contents and do they exist:<br>
#directoryList(directory).reduce((buffer="", filePath) => buffer & "#filePath#: #fileExists(filePath)#<br>")#
</cfoutput>

And this is its output:

Relative file path: [../getCanonicalPathIssue/targetfile.cfm]
Does relative path exist: [false]

Expanded file path: [/var/www/public/nonWheelsTests/getCanonicalPathIssue/targetfile.cfm]
Does expanded path exist: [true]

Canonical path from relative path: [/var/www/public/nonWheelsTests/getCanonicalPathIssue/targetFile.cfm]
Does canonical path from relative path exist: [false]

Canonical path from expanded path: [/var/www/public/nonWheelsTests/getCanonicalPathIssue/targetfile.cfm]
Does canonical path from expanded path exist: [true]

Directory: /var/www/public/nonWheelsTests/getCanonicalPathIssue/
Directory contents and do they exist:
/var/www/public/nonWheelsTests/getCanonicalPathIssue/test.cfm: true
/var/www/public/nonWheelsTests/getCanonicalPathIssue/targetfile.cfm: true

Basically I've got a second file (targetfile.cfm) in the same directory as I'm running this code from (test.cfm), and I'm giving Lucee a (valid) relative path to it, and asking some questions. Everything goes OK until I get to the result of getCanonicalPath(relativeFilePath): it's upper-cased part of the file name!??! Also note that expandPath(relativeFilePath) gets it right. And that getCanonicalPath(expandedPath) also gets it right.

The last bit of the code just shows what's def in the directory concerned.

I actually know what's causing the problem. A coupla hours ago I renamed the file from targetFile.cfm to targetfile.cfm. note the change in capitalisation, and how the old version matches the bad value getCanonicalPath is coming up with. So Lucee is caching that for some reason. I also found where to uncache it, but am buggered if I know why this setting caches file paths:

That's supposed to be about how often the contents of the file are checked; nothing about file paths. But if I set it to "Always (Bad)", then the problem goes away. It's clearly this lesser used definition of "bad", that means "actually works".

So that's an hour or so of my life I'll never get back. Thanks.


For shits and giggles I decided to run this on ColdFusion:

I'd give ColdFusion a pass here for choking on a relative path if not for two things:

  • it's specifically documented as dealing with them: "Absolute or relative path of a directory or to a file."
  • The path is a valid relative path. There's no trickery with the casing going on here, or caching or anything like that. The relative path I'm providing is the path to that file, relative to where the path is being used.

Oh well. I suppose I'll check with ppl to see if there's a reason why this behaviour on each platform, isn't wrong in the way I claim it is… and then raise some bugs. I'll cross ref once I've done that, but it'll be tomorrow now.

Righto.

--
Adam

Tuesday, 4 May 2021

Short version: getting CFWheels working outside the context of a web-browsable directory

G'day:

This is all extracted from my earlier article "Installing and running CFWheels in my Lucee Docker container" from a few days ago. Why am I repeating it? Because it kinda got lost in the morass of my other witterings, but it's probably reasonable information to have as a stand-alone guide.

This is also a living document. The steps here work for the bits of CFWheels I've used so far, but perhaps need tweaking as I delve deeper into it. I'll keep it up to date wth my findings. If you spot anything additional I need to do, let me know. I suspect there might be some config settings in the last section that might need adjustment. The code that uses those settings is pretty impenetrable, so I figured I'd discover by experimentation, not wading through the CFWheels codebase.

This still looks like a chunk of instructions, but it boils down to this:

  1. point your web root at the correct directory on both the CFML server and web server;
  2. get the CFWheels code;
  3. move it around a bit;
  4. do some source control;
  5. add some server mappings;
  6. add some application mappings;
  7. change some CFWheels config.

It's easy.

What?

CFWheels official installation guidance is to put all its files in a web browsable directory. I would never generally install a web application like this, and nor should you.

Why?

There are, conventionally, three components to the code in a web app:

  • Elements that need to be web browsable. Primarily images, JS, CSS and other assets the web server needs to serve to present the website. And secondary to that, the entry point to the web application. In the case of a CFML app this is a stub index.cfm and Application.cfc (both of which point back to the application code, and don't do much else other than that).
  • The web application. This is your CFML code. The stuff in source control, and the the stuff all your tests hit.
  • Third-party code. Stuff like the frameworks you use, and other libraries you might need along the way. You simply use this code, you do not maintain it.
  • OK, so a possible fourth: tests. These are usually separate from the application source code.

These are three different things, and they belong in three different places, for both security and management reasons. They should not be munged together, and it's not a good approach to suggest that they ought to be.

Where?

There's no standard in the CFML world that I am aware of, so in this case I am borrowing from how composer handles it in PHP applications. This puts third party code in a vendor directory, categorised by the vendor name within that (and the specific project from that vendor within that). I have this (very summarised):

/var/www/
├── public/
│  ├── images/
│  ├── javascripts/
│  ├── stylesheets/
│  ├── Application.cfc
│  └── index.cfm
├── src/
│  ├── controllers/
│  ├── models/
│  ├── views/
│  └── Application.cfc
├── test/
│  ├── functional/
│  ├── integration/
│  ├── unit/
│  └── Application.cfc
└── vendor/
  ├── cfwheels/
  └── testbox/
  • public is the web site's webroot. The other directories listed here are not web accessible. In fact they're not even present on the web server.
  • I did not see the need to have the vendor-company directory within vendor, so have just gone straight to the app level with cfwheels and testbox
  • It doesn't matter where you put the third-party stuff, as long as it's not public, and not muddled in with your own code. You could use org/cfwheels/cfwheels and com/ortussolutions/testbox if you wanted to. Or some other sensible schema.

How?

Step 1: ensure only the public directory is web browsable

I'm using Lucee via their official Docker container here, but the reconfiguration requirements (if not exact implementation) will apply on ColdFusion too, as well as Lucee installed via other mechanisms.

The general configuration of a CFML server is to create some directory (in my case /var/www), and say "this is the directory that's web browsable, stick you code in there". This is bad default advice, and encourages bad and insecure practice as that default. We only want the public directory of our app to be web browsable; not the whole codebase! Fortunately thisis reasonably easily remedied. The web-browsable directory is set in $TOMCAT_HOME/config/server.xml. For me this is /usr/local/tomcat/conf/server.xml. In that file there is this section:

<Host name="127.0.0.1"  appBase="webapps" unpackWARs="true" autoDeploy="true">
    <Valve className="org.apache.catalina.valves.RemoteIpValve"
         remoteIpHeader="X-Forwarded-For"
         requestAttributesEnabled="true" />
    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
         prefix="localhost_access_log" suffix=".txt"
         pattern="%h %l %u %t &quot;%r&quot; %s %b" />

    <Context path="" docBase="/var/www">
        <JarScanner scanClassPath="false"/>
    </Context>
</Host>

Change the /var/www to /var/www/public.

I've handled this for my Docker container by grabbing the file, saving it in my build context, and copying it across when I'm building the container (docker/lucee/Dockerfile):

COPY ./server.xml /usr/local/tomcat/conf/server.xml

Configuring your web server is outwith the remit of this article, but your website's doc root should be the public directory too, obviously.

This is also not related to this exercise, but whilst we're mentioning web servers, also follow the CFWheels docs when it comes to configuring rewrites from the CFWheels end of things. But I basically needed to add this to the bottom of config/settings.cfm:

set(URLRewriting="On")
set(rewriteFile="index.cfm")
Step 2: getting the CFWheels code

I used CommandBox to install CFWheels from Forgebox. This is my box.json:

{
    "dependencies":{
        "cfwheels":"^2.2.0"
    },
    "devDependencies":{
        "testbox":"^4.2.1+400"
    },
    "installPaths":{
        "testbox":"vendor/testbox/",
        "cfwheels":"vendor/cfwheels/"
    },
    "testbox":{
        "runner":"http://localhost:8888/test/runTests.cfm"
    }
}
Step 3: separating the CFWheels application implementation stubs from the framework application

All the CFWheels code is now in vendor/cfwheels. Some of this code is the basis for our application which we need to change; some of it is the CFWheels framework code. We need to copy some files to different more appropriate locations. All source file references below are relative to vendor/cfwheels. Destination locations are relative to the /var/www/ directory shown above.

  • index.cfm and rewrite.cfm need to be copied to public/
  • Application.cfc contains only an include for /wheels/functions.cfm. Copy only this include statement to be the last statement of src/Application.cfc.
  • tests/ should be copied to /var/www (adjacent to src/). These need to be source controlled, as they will contain your test code.
  • All the other subdirectories other than wheels/ of vendor/cfwheels should be copied to src/. These need to be source controlled, as they will contain your application code.
Step 4: omit src/wheels from source control
# .gitignore
/src/wheels/
Step 5: copy vendor/cfwheels/wheels to src/ during deployment

I'm using Docker for this, but as some separate step when deploying your code, you need to put the un-source-controlled vendor/cfwheels/wheels into src/. This is clunky and this directory does not belong in the middle of your application code, but there are a couple of hard-coded touch points to your source code within the wheels codebase, and try as I might, I could not work out how to override them.

WORKDIR  /var/www

RUN git clone git@github.com:adamcameron/cfml-in-docker.git .

RUN mkdir -p vendor
RUN box install

RUN cp -R /var/www/vendor/cfwheels/wheels /var/www/src/wheels
Step 6: Server mappings

Add some mappings that need to be at server level (not in Application.cfc), because Application.cfc files need them internally. I have done this with CFConfig, but you could do it via the admin UI:

{
    "CFMappings":{
        "/cfmlInDocker":{
            "inspectTemplate":"once",
            "physical":"/var/www/src",
            "primary":"physical"
        },
        "/test":{
            "inspectTemplate":"once",
            "physical":"/var/www/test",
            "primary":"physical"
        },
        "/wheels":{
            "inspectTemplate":"once",
            "physical":"/var/www/src/wheels",
            "primary":"physical"
        }
    }
}

My application's code is referenced with the mapping prefix /cfmlInDocker.

public/Application.cfc needs to extend cfmlInDocker.Application (ie: the one in the src/ directory). It can/should be otherwise empty.

We need the /wheels mapping to accommodate an include to a file on that path that we needed to put in src/Application.cfc.

TestBox's test runners all work via HTTP requests, so unfortuately the /test directory needs to be web accessible (these mappings are for code location, and also make the resources web-accessible on the internal CFML web server). They should not be exposed on your actual web server, so make sure they are blocked there.

Step 7: add application mappings to src/Application.cfc

The tail of your src/Application.cfc should end like this:

    thisDirectory = getDirectoryFromPath(getCurrentTemplatePath())

    this.mappings["/public/wheels"] =  getCanonicalPath("#thisDirectory#wheels")
    this.mappings["/app/wheels"] = getCanonicalPath("#thisDirectory#wheels")

    this.mappings["/app/controllers"] = getCanonicalPath("#thisDirectory#controllers")
    this.mappings["/app/models"] = getCanonicalPath("#thisDirectory#models")
    this.mappings["/app/events"] = getCanonicalPath("#thisDirectory#events")
    this.mappings["/app/files"] = getCanonicalPath("#thisDirectory#files")
    this.mappings["/app/plugins"] = getCanonicalPath("#thisDirectory#plugins")
    this.mappings["/app/views"] = getCanonicalPath("#thisDirectory#views")

    testDirectory = getCanonicalPath("#thisDirectory#../tests")
    this.mappings["/public/tests"] = testDirectory
    this.mappings["/app/tests"] = testDirectory
    this.mappings["/tests"] = testDirectory

    include "/wheels/functions.cfm";
}

This is so Wheels's internal CFC references can find your application code. You might wonder why we need all those individual "sub"-mappings of /app, instead of just one mapping to /app. This is because somewhere in the bowels of /wheels/functions.cfm that mapping is created, and given hard-coded assumptions about the codebase layout, it ends up pointing to the wrong place. At the same time, that code also used the paths I map above. Fortunately /app is never used by itself; it's always with one of those suffixes above.

Step 8: remap some include paths in src/config/settings.cfm

Add this lot to the bottom of src/config/settings.cfm:

set(eventPath = "../src/events")
set(filePath = "../src/files")
set(modelPath = "../src/models")
set(modelComponentPath = "../src/models")
set(pluginPath = "../src/plugins")
set(pluginComponentPath = "../src/plugins")
set(viewPath = "../src/views")

set(controllerPath = "/app/controllers")

set(webPath = "")
set(imagePath = "images")
set(javascriptPath = "javascripts")
set(stylesheetPath = "stylesheets")

set(wheelsComponentPath = "cfmlInDocker.wheels")

Values that "worked" here were all set via trial and error, because the CFWheels code is very… opaque, and there's no documentation I can find on them. Certainly I needed these values for the controllers and views to be located (that's as much as I've looked at with CFWheels so far). I expect the component paths need to be dotted rather than a file-system path: I'll try to find out this evening.

Conclusion

Ideally at some juncture CFWheels will be reconfigured to not assume where the application implementation code is, and use a mapping (that it is provided at bootstrap) to find it instead. And similarly path all its own code on a /wheels mapping, then the framework could reside anywhere. It should just need to work like this:

// my Application.cfc
component extends=wheels.Application {

    this.appMapping = "/myApp"
}

And in the setup docs say "make sure to create these two server mappings". It's just easy. Which is what CFWheels is supposed to be all about, right?

Righto.

--
Adam

abort! abort;!

G'day

What do you (CFMLers, sorry) make of this?

<cfscript>
function f(){
    writeOutput("in f<br>")
    abort
}

writeOutput("before f<br>")
f()
writeOutput("after f<br>")
</cfscript>

What would you expect that to output? The foolhardly money would go on:

before f
in f

Because, like, it's obvious: after f is never called because we're aborting in the function. Right?

Well yer partly right. On ColdFusion, that's exacly what happens (I tested on CF2021). On Lucee, however, I get this:

before f
in f
after f

Um… kewl.

The thing is that if one adds a semi-colon after the abort, Lucee starts behaving.

<cfscript>
function f(){
    writeOutput("in f<br>")
    abort;
}

writeOutput("before f<br>")
f()
writeOutput("after f<br>")
</cfscript>

I had some abort-confusion the other day on the CFML / Lucee Slack channel because this had behaviour I did not expect:

But that's actually correct behaviour. abort takes an optional string parameter showError, and writeOutput returns true (don't ask), which can be coerced into a string. If a statement has only one parameter, its name can be omitted, so Lucee is interpretting that as a multi-line abort statement - with a showError value of true - that doesn't end until the explicit semi-colon.

This is not the same though, as far as I can tell. The } of the function block is an explicit end-of-statement token just like a semi-colon is. Or at least it ought to be.


In other news, this sample code won't run correctly on trycf.com, which you can check out at https://trycf.com/gist/759318ea257c7ca7130c1f12c3ee72f8: Lucee doesn't work as expected whether or not the semi-colon is there; and CF only works as expected when the semi-colon is not there. This is at odds to how it works on (my) actual servers. I'd be interested in what behaviour you get on your servers?

Am still wondering a bit if I'm missing something here; especially given the different varieties of behaviour…

Righto.

--
Adam

Monday, 3 May 2021

Testing: A Horror Story

G'day:

My adventures in Lucee / CFWheels / Docker continues. This time I'm looking at what CFWheels offers by way of testing.

2021-05-04 - editorial update

In this article I am very - but I feel justifiably - harsh about RocketUnit (you need to read down a bit before you get to this). I'm also harsh towards the CFWHeels team's historical decision to include it in CFWheels. I really do think that was a poor decision. But so be it, it's done: other than mentioning it, there's no point me dwelling on it beyond that.

Some of the tone and wording of this article has be read as an indictment of CFWheel and/or its team. Beyond that initial poor decision, all my opprobrium here is directed at RocketUnit, not CFWheels, and not the team. I can see how people would think otherwise though.

To be clear: this article is largely about RocketUnit. CFWheels was just the path via which I came to know about it.

I'm going to be brief, because it's so appalling I don't want to waste too much time on it. It can be summarised thus:

Are you f***ing joking?

Firstly I'm gonna quote the CFWheels docs (Testing Your Application › The Test Framework):

Like everything else in CFWheels, the testing framework is very simple, yet powerful. You don't need to remember a hundred different functions because CFWheels' testing framework contains only a handful.

(my emphasis there)

Sorry but I see wording like this implying something about CFML developers' capabilities all the time, and it really annoys me. To me that statement is patronising as well as disingenuous. It plays on this perception that somehow CFML devs aren't capable of doing anything unless it's reeeeally easy, and that that is just how things should be. Can we please not? Can we please not normalise CFML devs as somehow not being able to cope with a small amount of complexity if it will ultimately assist them growing as developers? It's also a bit disingenuous because no testing framework requires one to learn any more than a handful of methods, and learning how to write code is our job. It suggests testing tooling is somehow usually hard (which it just isn't).

Next I read further into the docs:

Evaluation

assert(): This is the main method that you will be using when developing tests. To use, all you have to do is provide a quoted expression. The power of this is that ANY 'truthy' expression can be used.

An example test that checks that two values equal each other:

function testActualEqualsExpected() {
    actual = true;
    expected = true;
    assert("actual eq expected");
}

I'm sorry, come again? You. Give. It. A. Quoted. Expression. It's using evaluate(), isn't it? Now I'm not actually fundamentally against evaluate as a thing. Not like some of the CFML community groupthink. It has its place, just that that place is seldom "in your code": one hardly ever needs to use it. I can't wait to see why it's being used here. Other than, like, cos for some reason the assert function expects a string, so it needs to be evaluated to even work.

And "[the] power of this is that ANY 'truthy' expression can be used"? So: just like any other implementation of assert that any other testing framework ever written. Except none of those need to pass a string to the assert function. This is not a MSP of this testing framework, and the implementation we're being shown is inferior to any other testing framework I've seen. I won't look at the actual code for this just yet, as there's still more horror to see in the guidance docs first.

I'm going to wind back up the docs a bit now:

Do not var-scope [any] variables used in your tests. In order for the testing framework to access the variables within the tests that you're writing, all variables need to be within the component's variables scope.

When I first read that I was like "oh yer having a laugh, right?" and it was not until I got to the "boolean expression as a string" and "evaluate" that it suddenly made "sense". Because the string expression needs to be evaluated within the implementation of assert, of course the variables in the "expression" can't be function-local to your test: assert's code won't be able to see those. So now because the implementation has chosen to pass the test expression as a string, it's forcing us to write bad, flaky, fragile test code, with variables bleeding all over the place by design. I can't really see how this framework could ever be used seriously outside a proof of concept or other very superficial environment. It'd drive the devs batty.

And now I hasten to add, the rest of the CFWHeels docs on testing are actually pretty helpful when it focuses-away from the test framework, and back to CFWheels stuff.

So what's with this test code? What is going on with this assert function? Oh yeah, btw: assert is the only assertion this framework offers. So there's goes your elegant, expressive, easy to understand test code. It's just a bunch of vanilla assert calls now. This is a telling stark contrast every other test framework out there that has decided to implement a lot of different assertions, and sees this as a good thing. I mean obviously at the end of the day they are all asserting if something is true; if you look at the internal implementations, generally all the facade assertions end up doing just that: calling a central "assert-true" assert method. The situation-specific assertions are there to make your test code easier and simpler to understand, which is vital in testing. I cannot understand why there was a perception that it was a good thing for the framework to have only one assertion.

Right. The code. Which is all in wheels/test/functions.cfm (nice file name there, CFWheels Team :-|. And I also have yet to work out why CFWheels is designed such that all the code for its .cfc files are implemented in .cfm files: this is a question for another day).

This is interesting:

Copyright 2007 RocketBoots Pty Limited - http://www.rocketboots.com.au

OK so here's where I clock that CFWheels have just bundled this "RocketUnit" thing into the framework. OK so in defence of the CFWheels team, this is possibly just a really poor decision to include this, rather than the team actively writing this… this… thing. I note that CFWheels (in 2021) still bundles only v1.0 of RocketUnit (from something like 2007).

And here we go:

public void function assert(required string expression) {
    // other stuff snipped
    if (!Evaluate(arguments.expression)) {

And why is it doing this? This is hilarious. The way this code has been implemented, and the reason that one needs to pass a string to assert is because if the assertion fails, then the code basically picks through the string, does a primitive tokenisation effort to find dynamic expressions, and then evaluates them again to get values to return in the assertion-failed message. EG; if your expression is "x == 3" and variables.x is 4, it'll re-evaluate each element of the string so it can say something like "uh-oh x was 4 not 3". And the entire reason it needs to do this with an equality-check is it's shot itself in the foot by only having assert, instead of taking the obvious route of having an equality assertion that takes two values to compare. Neither of which need to be in a global scope; neither of which need to be re-evaluated strings, because the assertion was passed their values. It could be called, I dunno, assertEquals or something.

It actually gets better (read: "worse"). In v2.x of RocketUnit, there's no need for the quoted string any more, because what the assertion implementation does when the assertion fails is it thows a fake exception to generate a callstack, and then crawls its way up that to try to find the line of code that called assert, and extract the expression from that. Seriously, have a look:

try {
    throw(errorCode=DUMMY_ERRCODE);
} catch(any) {
    // assert is one stack frame up from this function [1], therefore [2]
    source = cfcatch.tagContext[2].codePrintPlain;
    startLineNumber = lineNumber = cfcatch.tagContext[2].line;
}

If you look further down, you can see how the implementation itself knows how flaky its own approach even is, given the various places the code that tries to extract the expression needs to bail out.

Argh!

When discussing this with another CFML community member, they sent me this:

* see footnote regarding usage of this image

I think this is something that should have set-off alarm bells when the RocketUnit project first started… evolving.


Here's how it would seem that one needs to safely write tests with this shambles:

component extends="app.tests.Test" {

    function testBasicRoutingWorks() {
        variables.response = processRequest(
            params = {
                controller = "testroute",
                action = "debug"
            },
            returnAs = "struct"
        )
        try {
            assert("variables.response.status eq 200")
            assert("variables.response.body contains 'EXPECTED_CONTENT'")
        } finally {
            structDelete(variables, "response")
        }
    }
}

One can't just leave variables-scoped variables lying around the place in test code, so you need to get rid. Test code needs to be well encapsulated and robust and not prone to interference from unexpected avenues. Or one could just have one test per CFC. Or one could painstakingly make sure that tests don't accidentally share variables. Or hey just suck it and see (I suspect this is what RocketUnit expects). It's just too easy to accidentally not reinitialise a variable and be using an uncontrolled value from another test in a subsequent one.

Do not var-scope [any] variables

I have to wonder why - at the point one realises one needed to document something that contravenes standard good coding practice that languages go out of their way to accommodate - the person didn't go "ah now lads we've f***ed this one", and rethink things.


Do me a favour if you're using CFWheels? Don't use its in-built testing framework. It's shite. Use TestBox instead. It's dead easy to use, still supported, still being actively developed on a daily basis, and makes for really nice easy to read, easy to develop test code. There's a strong and willing community out there to help you if you get stuck with anything. It'll also be a cinch to test yer CFWheels work with, as it's completely situation-agnostic.

And to the CFWheels team: please consider just ripping this out of CFWheels wholesale. This is really letting your framework down, I think.

Righto.

--
Adam

* I am not the copyright holder of this image. I'm hoping I'm covered by "fair use" here, but if you are the copyright holder and disagree: let me know, and I'll replace it with something else.

Installing and running CFWheels in my Lucee Docker container

G'day:

2021-05-05

Some of the way I've needed to configure things to get CFWheels working across the board have been tweaked/augmented from what I have in this article. A stripped down version of the steps I currently have is at "Short version: getting CFWheels working outside the context of a web-browsable directory". I can't be arsed rewriting this every time I find something new I need to tweak, but I'll keep that other article up to date.

If you've been following along in the previous articles in this series (Lucee / CFWheels / Docker), You'll know I have a set of Docker containers running Nginx, MariaDB and Lucee. I have been configuring this lot using a TDD approach, so I have a suite of tests that demonstrate everything is integrated nicely. Now it's time to install CFWheels as well, and have a look at it. I will be continuing my TDD approach to doing this work, so will list the cases I'm addressing at the heading of each step. Update, two weeks later: no I won't. This took so bloody long and was such a pain in the arse, I'm not gonna bother with that.

Overall aim: "It serves the CFWheels welcome page"

Reading ahead through the CFWheels › Installation › Test it docs, I see I should expect a welcome page when I browse to the site, all going well. So I will write a test to expect this (CFWheelsTest.cfc):

component extends=testbox.system.BaseSpec {

    function run() {
        describe("Tests CFWheels app installation", () => {
            it("serves the CFWheels welcome page after installation", () => {
                http url="http://cfml-in-docker.frontend/" result="response";

                expect(response.status_code).toBe( 200, "HTTP status code incorrect")
                expect(response.fileContent.trim()).toBe( "NOT SURE YET", "Response body incorrect")
            })
        })
    }
}

I dunno what form this welcome will take as yet, so I've just got some text that definitely won't be right, and will start with that. I'd improve this later when I know what to check for.

Attempting (spoilers: and failing) to install using CFWheels CLI

Looking at the CFWheels installation docs, I should use CommandBox to do the installation from ForgeBox, so I'll give that a go. I've already got CommandBox installed to run my tests. One thing I've noticed from reading ahead, the default location the files for CFWheels get put in a manual install is just slapped in the web root. That's… erm… "less good than it could be", and I won't be doing this. I've already gone through this with TestBox, so you can read why in that earlier article: "Adding TestBox, some tests and CFConfig into my Lucee container › It needs to only expose public stuff to the public"

I can tell CommandBox where to install things easily enough:

root@efe37f109b87:/var/www# box install id=cfwheels-cli directory=vendor
√ | Installing package [forgebox:cfwheels-cli]
root@efe37f109b87:/var/www#

And this has updated my box.json for me:

root@efe37f109b87:/var/www# cat box.json
{
    "dependencies":{
        "cfwheels-cli":"^0.4.0"
    },
    "devDependencies":{
        "testbox":"^4.2.1+400"
    },
    "installPaths":{
        "cfwheels-cli":"vendor/cfwheels-cli/",
        "testbox":"vendor/testbox/"
    },
    "testbox":{
        "runner":"http://localhost:8888/test/runTests.cfm"
    }
}
root@efe37f109b87:/var/www#

So far… so good.

The way the docs are recommending I set up my new CFWheels app is through this CLI thing I just installed, by going box wheels new. It sounds to me like this is going to assume it can do whatever it likes in its install directory, so to start with I'm just going to do the install in a temp directory and have a look at what it does, and whether it will collide with my existing work. It's nae bother if it does, I'll move my stuff around, but I just want to know what I'm getting myself into first.

root@efe37f109b87:/var/tmp/cfwheels# pwd
/var/tmp/cfwheels
root@efe37f109b87:/var/tmp/cfwheels# box wheels new
ERROR (5.2.1+00295)

Command "wheels new" cannot be resolved.

Please type "help" for assistance.

root@efe37f109b87:/var/tmp/cfwheels#

OK. Not off to a good start. I gulped and wondered whether this needs to be run from within the directory I did the install, so I switched over to /var/www/vendor/cfwheels-cli and tried there: same result. I also tried doing it from within CommandBox's special wee shell, but the results were the same.

I tried to see what help was on offer for CFWheels in general:

CommandBox> wheels help

**************************************************
* CommandBox Help for wheels
**************************************************


Command [wheels] not found.

Fine. OK, look: I don't know what's going on here; and to be completely honest, I don't care. The manual installation process is "stick the files in a directory", and I can pretty much manage that by myself (cough: don't speak too soon, Cameron). I don't think I need the Wheels CLI to do this for me. Or, as is the case here: not do it for me. De-installing that.

Installing just CFWheels

I'll still use ForgeBox to install the app though:

root@efe37f109b87:/var/www# box install id=cfwheels directory=vendor/cfwheels
√ | Installing package [forgebox:cfwheels]
root@efe37f109b87:/var/www#

One thing to note here is that I needed to specify the installation subdirectory for CFWheels too, ie: directory=vendor/cfwheels. When I installed Testbox, I only had to specify the base directory, ie: directory=vendor, and the testbox subdirectory was created for me within that. Now I don't mind whether I need to not specify the directory the installation actually goes in, or whether I don't need to specify it. But it should be one or the other. I'm not sure what's going on here.

Anyway, having done this, I see all the files in a subdirectory how one might want. Kinda:

root@efe37f109b87:/var/www# ll vendor/cfwheels/
total 88
drwxr-xr-x 16 root root 4096 Apr 25 18:35 ./
drwxr-xr-x  1 root root 4096 Apr 25 18:35 ../
-rwxr--r--  1 root root   67 Nov 22 09:57 Application.cfc*
-rwxr--r--  1 root root  975 Apr 25 18:10 box.json*
drwxr-xr-x  6 root root 4096 Apr 25 18:35 config/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 controllers/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 events/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 files/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 global/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 images/
-rwxr--r--  1 root root   52 Nov 22 09:57 index.cfm*
drwxr-xr-x  2 root root 4096 Apr 25 18:35 javascripts/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 miscellaneous/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 models/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 plugins/
-rwxr--r--  1 root root   52 Nov 22 09:57 rewrite.cfm*
-rwxr--r--  1 root root  162 Nov 22 09:29 root.cfm*
drwxr-xr-x  2 root root 4096 Apr 25 18:35 stylesheets/
drwxr-xr-x  4 root root 4096 Apr 25 18:35 tests/
drwxr-xr-x  2 root root 4096 Apr 25 18:35 views/
drwxr-xr-x 14 root root 4096 Apr 25 18:35 wheels/
root@efe37f109b87:/var/www#

Most of that stuff is stub files for my Wheels application; except for the wheels directory which is the actual CFWheels application. I expected this, because the default approach to installing this thing is - remember - "slap it all in the web root". I'll deal with this next, but first I see a problem with box.json:

root@efe37f109b87:/var/www# cat box.json
{
    "dependencies":{
        "cfwheels":"^2.2.0"
    },
    "devDependencies":{
        "testbox":"^4.2.1+400"
    },
    "installPaths":{
        "testbox":"vendor/testbox/"
    },
    "testbox":{
        "runner":"http://localhost:8888/test/runTests.cfm"
    }
}
root@efe37f109b87:/var/www#

It's not put the installPath in for CFWheels. This is no good when I come to use this for a Dockerised deployment. I will quickly manually edit this file and do a rebuild and see what happens. Back in a few min.

(Oh I also note that CommandBox has written "vendor/testbox/" in there, despite me saying just "vendor". I mean… fine…: but why did it not do the same for CFWheels?)

Cool so I did the rebuild, and having put the installation path for CFWheels into box.json manually, it all seemed to work:

root@78cb5ad3bf2c:/var/www# ll vendor
total 16
drwxr-xr-x  1 root root 4096 Apr 25 18:47 ./
drwxr-xr-x  1 root root 4096 Apr 25 18:47 ../
drwxr-xr-x 16 root root 4096 Apr 25 18:47 cfwheels/
drwxr-xr-x  9 root root 4096 Apr 25 18:47 testbox/
root@78cb5ad3bf2c:/var/www# ll vendor/cfwheels/
total 84
drwxr-xr-x 16 root root 4096 Apr 25 18:47 ./
drwxr-xr-x  1 root root 4096 Apr 25 18:47 ../
-rwxr--r--  1 root root   67 Nov 22 09:57 Application.cfc*
-rwxr--r--  1 root root  975 Apr 25 18:47 box.json*
drwxr-xr-x  6 root root 4096 Apr 25 18:47 config/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 controllers/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 events/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 files/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 global/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 images/
-rwxr--r--  1 root root   52 Nov 22 09:57 index.cfm*
drwxr-xr-x  2 root root 4096 Apr 25 18:47 javascripts/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 miscellaneous/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 models/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 plugins/
-rwxr--r--  1 root root   52 Nov 22 09:57 rewrite.cfm*
-rwxr--r--  1 root root  162 Nov 22 09:29 root.cfm*
drwxr-xr-x  2 root root 4096 Apr 25 18:47 stylesheets/
drwxr-xr-x  4 root root 4096 Apr 25 18:47 tests/
drwxr-xr-x  2 root root 4096 Apr 25 18:47 views/
drwxr-xr-x 14 root root 4096 Apr 25 18:47 wheels/
root@78cb5ad3bf2c:/var/www#

Now my challenges start.

Reorganising the file structure

I have to take that listing above, and reorganise it as follows:

adam@DESKTOP-QV1A45U:/mnt/c/src/cfml-in-docker$ tree . --dirsfirst
.
├── public
│   ├── images
│   ├── javascripts
│   ├── stylesheets
│   ├── Application.cfc
│   ├── index.cfm
│   └── rewrite.cfm
├── src
│   ├── config
│   ├── controllers
│   ├── events
│   ├── files
│   ├── global
│   ├── miscellaneous
│   ├── models
│   ├── plugins
│   ├── views
│   ├── Application.cfc
│   └── root.cfm
├── tests
│   ├── functions
│   │   └── Example.cfc
│   ├── requests
│   │   └── Example.cfc
│   └── Test.cfc
└── vendor
   └── cfwheels
       └── wheels


Getting CFWheels to work with that file structure

A couple of week has passed since I wrote the preceding paragraph. In the interim I've written a couple of articles about some side challenges I have had as I progressed this work. They weren't directly-related enough to what I'm talking about here to include them, hence I've published them separately:

Along the way I've enhanced the testing I've had to do to ensure my tinkerings worked and didn't break anything. As it currently stands I have a CFWheels-driven site "working": I have created some test endpoints that behave as I expect, and I can run its internal test suite. That's a pretty light definition of "working", I know.

I'll continue to detail the CFWheels config changes I needed to make to get everything working…

After doing the work for that "How TDD and automated testing helped me…" article above, I had a decent battery of tests that acted as a safety net for expectations I had for my site URLs:

I'm showing you the status as they are now, after I've done the work; obviously as the work was under way they weren't all a) there; b) passing. I just can't be bothered rolling my code back to show them that way, and in a way this is a more useful listing anyhow.

Oh and ignore the skipped ones. There's a bug in CFWheels that prevents those from working properly: the 404 page in dev more returns a 200-OK. I've left the tests skipped as a reminder I need to revisit them once the bug is fixed. I actually note it's been fixed (nice work!), but the version it's in ain't up on ForgeBox yet.

So… how did I get the thing to work?

Well. Firstly. How did I not get it to work. My expectations were that any well-designed third-party app would not make assumptions about where it was in the file system; or where any files implementing it happened to be. All I expected to have to do here is to set a mapping for CFWheels, and a mapping for my app / implementation code, tell my code where the CFWheels application is and then it'd just work. However I'm afraid CFWheels is not a very well-designed application, so that was a non-starter.

Core CFWheels code has been implemented so that it will only work if the wheels directory is right slap-bang in the middle of my implementation code. Primarily this line of code in wheels/events/onapplicationstart.cfm:

$include(template = "config/settings.cfm");

And that weirdo (name and implementation) $include function does this to it:

<cfinclude template="../../#LCase(arguments.template)#">

Neat. Let's dispense with our carefully-cased file names, cos we'll enforce lower-case on them all. That's definitely the job of a function called "include". Sorry "$include". And we'll just force the all to be two driectories up from wheels/global because isn't that where everybody puts their files?

I'm going to need to put that wheels directory into the middle of my source-controlled application code files, are't I? Sigh. I'm not checking it in to source control though: that would be too far beyond the pale. I'm installing it to where it belongs: vendor, and then getting Docker to copy it into my source-controlled directory once it's there:

RUN git clone git@github.com:adamcameron/cfml-in-docker.git .

# irrelevant stuff snipped

RUN mkdir -p vendor
RUN box install

RUN cp -R /var/www/vendor/cfwheels/wheels /var/www/src/wheels

I'm also .gitignore-ing /src/wheels/ so it doesn't accidentally end up in source control.

I know this is a really jerry-built, but… well: my hands are kind of tied.

The next step can be sorted out via config settings CFWheels exposes via config/settings.cfm:

srcPrefix = "../src"

set(eventPath = "#srcPrefix#/events")
set(filePath = "#srcPrefix#/files")
set(modelPath = "#srcPrefix#/models")
set(modelComponentPath = "#srcPrefix#/models")
set(pluginPath = "#srcPrefix#/plugins")
set(pluginComponentPath = "#srcPrefix#/plugins")
set(viewPath = "#srcPrefix#/views")
set(controllerPath = "#srcPrefix#/controllers")

set(webPath = "")
set(imagePath = "images")
set(javascriptPath = "javascripts")
set(stylesheetPath = "stylesheets")

set(wheelsComponentPath = "cfmlInDocker.wheels")

This is almost OK. Except for the headless-function use of set, which appears in here as if by magic; and for some unfathomable reason that it's implemented in a .CFM file not in a method of maybe a Settings.cfc file or something a bit more OOP. The fact that this .CFM file is ultimately included into a .CFC file makes the situation worse, not better, IMO. This is endemic in the application design of CFWheels btw.

Getting the testing framework to work

The other thing I needed to do is to add some mappings so the inbuilt CFWheels testing "framework" can actually run tests. This is all web-driven, btw. These had to go into src/Application.cfc

thisDirectory = getDirectoryFromPath(getCurrentTemplatePath())
this.mappings["/public/wheels"] =  getCanonicalPath("#thisDirectory#wheels")
this.mappings["/public/tests"] = getCanonicalPath("#thisDirectory#../tests")
this.mappings["/app/tests"] = getCanonicalPath("#thisDirectory#../tests")

You might as why I need to put these into my application's Application.cfc, and I couldn't put them in a tests/Application.cfc that happened to extend the app's one. Yes, this was my expectation as well: this crazy notion of OOP and separation of concerns. However CFWheels apparently runs its tests via a controller in your web app, then there isn't really a tests/Application.cfc that I can leverage.

After that the UI in the test framework was still breaking, creating links incorrectly. This ended up being because I needed to tell CFWheels that I am rewriting URLs. Not sure why, but there you go (Tom King set me straight on this, on the CFWheels subchannel of the CFML Slack channel: he is a star and has been a big help to me in all of this). The settings are also in config/settings.cfm:

set(URLRewriting="On")
set(rewriteFile="index.cfm")

And after doing that lot: all my TestBox tests were green! And there's even a test in there that tests that CFWheels' tests are working, which they are. I say "all of them", that just means the coupla stub tests they include as examples, and two wee tests I stuck in as proof-of-working. I'm not doing anything major in there. But it's green:

I'll not go into the implementation of those tests here. I'll look at that particular horror story in a separate article.

And the site

Tests are one thing, but I better demonstrate to you that the CFWheels-driven site is up and running too:



Conclusion

Detailing how to get this working took a lot less time to write (and for you to read) than it did for me to work out. It was not helped by my various Nginx-config-shortcomings; but I did have to dive into CFWheels's code more than I would have liked, and that was an exercise in frustration in itself as the way it's been implemented is… "obstructive". As I mentioned above Tom King patiently answered a bunch of questions for me, which was also a big help.

However here I am. I've got my app working. It would not surprise me if there were some other config tweaks I need to make down the track, but I can start doing some exercises of adding some of me own routes, controller, models, views in here now. I've worked through the CFWHeels tutorials and a bunch of their screencasts, and it seems easy enough to throw simple things together. I have my suspicions that as soon as requirements are no longer simplistic then the way CFWheels is designed / implemented might start getting in its own way, but I'll give it the benefit of the doubt for now.

Righto.

--
Adam

 

 

 

 

But wait! There's more!

Just after pressing "Publish" on this, I foolishly clicked about on the UI of the site home page. And I clicked on "Docs":

And the thanks I got for doing that was this:

I looked through the CFWheels code to see what it was ass-u-me`ing, and it's here in wheels/functions.cfm:

this.wheels.rootPath = GetDirectoryFromPath(GetBaseTemplatePath());

// ...

// Add mapping to the root of the site (e.g. C:\inetpub\wwwroot\, C:\inetpub\wwwroot\appfolder\).
// This is useful when extending controllers and models in parent folders (e.g. extends="app.controllers.Controller").
this.mappings["/app"] = this.wheels.rootPath;

And in this case, GetBaseTemplatePath() is the index.cfm file in the public, not the app root. So that won't do. I thought this might be a show stopper because I can't override that because of the way CFWheels is architected. However all was not lost because as far as I can tell that base /app mapping is never used by itself, it's only ever used as the base for app.controllers, app.models, and app.wheels. I can just make the correct mappings to those, in my Application.cfc. But first a quick test:

it("serves the Docs page", () => {
    http url="http://cfml-in-docker.frontend/wheels/docs" result="response";

    expect(response.status_code).toBe(200, "HTTP status code incorrect")
    expect(response.fileContent).toInclude("<title>Docs | CFWheels</title>")
})

Now we can fix it:

thisDirectory = getDirectoryFromPath(getCurrentTemplatePath())
this.mappings["/public/wheels"] =  getCanonicalPath("#thisDirectory#wheels")
this.mappings["/public/tests"] = getCanonicalPath("#thisDirectory#../tests")
this.mappings["/app/tests"] = getCanonicalPath("#thisDirectory#../tests")

this.mappings["/app/controllers"] = getCanonicalPath("#thisDirectory#controllers")
this.mappings["/app/models"] = getCanonicalPath("#thisDirectory#models")
this.mappings["/app/wheels"] = getCanonicalPath("#thisDirectory#wheels")

And that sorted it. I wonder what the next glitch is gonna be. But I'm gonna stop looking. For now…

RightoAgain.

--
AdamAgain