├── tests ├── resources │ ├── app │ │ ├── lib │ │ │ └── drop_jars_here.txt │ │ ├── models │ │ │ └── PutStuffHere.txt │ │ ├── views │ │ │ ├── api │ │ │ │ └── index.cfm │ │ │ ├── main │ │ │ │ ├── indexHelper.cfm │ │ │ │ └── index.cfm │ │ │ └── _templates │ │ │ │ ├── 404.html │ │ │ │ └── generic_error.cfm │ │ ├── includes │ │ │ ├── i18n │ │ │ │ └── i18n_goes_here.txt │ │ │ ├── helpers │ │ │ │ └── ApplicationHelper.cfm │ │ │ ├── images │ │ │ │ └── ColdBoxLogo2015_300.png │ │ │ └── fonts │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── interceptors │ │ │ └── PutStuffHere.txt │ │ ├── modules │ │ │ └── tracked_modules_here.txt │ │ ├── tests │ │ │ ├── results │ │ │ │ └── results_go_here.txt │ │ │ ├── specs │ │ │ │ ├── all_tests_go_here.txt │ │ │ │ ├── modules │ │ │ │ │ ├── module_tests_go_here.txt │ │ │ │ │ ├── unit │ │ │ │ │ │ └── unit_tests_go_here.txt │ │ │ │ │ └── integration │ │ │ │ │ │ └── integration_tests_go_here.txt │ │ │ │ └── integration │ │ │ │ │ ├── MainBDDTest.cfc │ │ │ │ │ └── MainTest.cfc │ │ │ ├── resources │ │ │ │ ├── HttpAntRunner.cfc │ │ │ │ └── RemoteFacade.cfc │ │ │ ├── runner.cfm │ │ │ ├── Application.cfc │ │ │ ├── test.xml │ │ │ └── index.cfm │ │ ├── modules_app │ │ │ └── custom_modules_here.txt │ │ ├── favicon.ico │ │ ├── config │ │ │ ├── .htaccess │ │ │ ├── Application.cfc │ │ │ ├── Router.cfc │ │ │ ├── WireBox.cfc │ │ │ ├── CacheBox.cfc │ │ │ └── Coldbox.cfc │ │ ├── index.cfm │ │ ├── .gitattributes │ │ ├── robots.txt │ │ ├── readme.md │ │ ├── handlers │ │ │ ├── Main.cfc │ │ │ └── api.cfc │ │ ├── Application.cfc │ │ └── layouts │ │ │ └── Main.cfm │ ├── chuck_norris.jpg │ └── ModuleIntegrationSpec.cfc ├── specs │ ├── integration │ │ ├── ModuleActivationSpec.cfc │ │ ├── IncompleteRequestsSpec.cfc │ │ ├── BuilderPassThroughSpec.cfc │ │ ├── FileUploadsSpec.cfc │ │ ├── RedirectsSpec.cfc │ │ ├── PostSpec.cfc │ │ ├── AsyncSpec.cfc │ │ ├── InterceptorsSpec.cfc │ │ ├── BuilderDefaultsSpec.cfc │ │ ├── DebugSpec.cfc │ │ ├── RetrySpec.cfc │ │ ├── GetSpec.cfc │ │ └── FakeSpec.cfc │ └── unit │ │ ├── HyperBuilderSpec.cfc │ │ ├── HyperRequestSpec.cfc │ │ └── HyperResponseSpec.cfc ├── runner.cfm ├── Application.cfc └── index.cfm ├── .gitignore ├── ModuleConfig.cfc ├── server-lucee@5.json ├── server-lucee@6.json ├── server-lucee@7.json ├── server-lucee@be.json ├── server-adobe@be.json ├── server-adobe@2023.json ├── server-adobe@2021.json ├── server-adobe@2025.json ├── server-boxlang@1.json ├── server-boxlang@be.json ├── server-boxlang-cfml@1.json ├── models ├── HyperHttpClientInterface.cfc ├── FakeHyperResponse.cfc ├── TestBoxMatchers.cfc ├── HyperBuilder.cfc ├── HyperResponse.cfc └── CfhttpHttpClient.cfc ├── LICENSE ├── README.md ├── box.json ├── .github └── workflows │ ├── cron.yml │ ├── pr.yml │ └── release.yml └── .cfformat.json /tests/resources/app/lib/drop_jars_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/models/PutStuffHere.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/views/api/index.cfm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/includes/i18n/i18n_goes_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/interceptors/PutStuffHere.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/modules/tracked_modules_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/tests/results/results_go_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/tests/specs/all_tests_go_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/modules_app/custom_modules_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/tests/specs/modules/module_tests_go_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/tests/specs/modules/unit/unit_tests_go_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/tests/specs/modules/integration/integration_tests_go_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldbox-modules/hyper/HEAD/tests/resources/app/favicon.ico -------------------------------------------------------------------------------- /tests/resources/chuck_norris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldbox-modules/hyper/HEAD/tests/resources/chuck_norris.jpg -------------------------------------------------------------------------------- /tests/resources/app/includes/helpers/ApplicationHelper.cfm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/tests/resources/HttpAntRunner.cfc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /testbox 2 | /tests/results 3 | /tests/resources/app/coldbox 4 | /node_modules 5 | /modules 6 | .engine 7 | jmimemagic.log 8 | 9 | .vscode -------------------------------------------------------------------------------- /tests/resources/app/tests/resources/RemoteFacade.cfc: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/resources/app/views/main/indexHelper.cfm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/resources/app/includes/images/ColdBoxLogo2015_300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldbox-modules/hyper/HEAD/tests/resources/app/includes/images/ColdBoxLogo2015_300.png -------------------------------------------------------------------------------- /tests/resources/app/config/.htaccess: -------------------------------------------------------------------------------- 1 | #apache access file to protect the config.xml.cfm file. Delete this if you do not use apache. 2 | authtype Basic 3 | deny from all 4 | Options -Indexes -------------------------------------------------------------------------------- /tests/resources/app/includes/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldbox-modules/hyper/HEAD/tests/resources/app/includes/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /tests/resources/app/includes/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldbox-modules/hyper/HEAD/tests/resources/app/includes/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /tests/resources/app/includes/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldbox-modules/hyper/HEAD/tests/resources/app/includes/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /tests/resources/app/includes/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coldbox-modules/hyper/HEAD/tests/resources/app/includes/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /tests/resources/app/config/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a protection Application cfm for the config file. You do not 3 | * need to modify this file 4 | */ 5 | component { 6 | 7 | abort; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | this.name = "hyper"; 4 | this.author = ""; 5 | this.webUrl = "https://github.com/elpete/hyper"; 6 | this.cfmapping = "hyper"; 7 | 8 | function configure() { 9 | interceptorSettings = { "customInterceptionPoints" : [ "onHyperRequest", "onHyperResponse" ] }; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /server-lucee@5.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-lucee@5", 3 | "app":{ 4 | "serverHomeDirectory":".engine/lucee5", 5 | "cfengine":"lucee@5" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "openBrowser":"false" 16 | } 17 | -------------------------------------------------------------------------------- /server-lucee@6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-lucee@6", 3 | "app":{ 4 | "serverHomeDirectory":".engine/lucee6", 5 | "cfengine":"lucee@6" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "openBrowser":"false" 16 | } 17 | -------------------------------------------------------------------------------- /server-lucee@7.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-lucee@7", 3 | "app":{ 4 | "serverHomeDirectory":".engine/lucee7", 5 | "cfengine":"lucee@7" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "openBrowser":"false" 16 | } 17 | -------------------------------------------------------------------------------- /server-lucee@be.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-lucee@be", 3 | "app":{ 4 | "serverHomeDirectory":".engine/luceeBE", 5 | "cfengine":"lucee@be" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "openBrowser":"false" 16 | } 17 | -------------------------------------------------------------------------------- /tests/resources/app/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure() { 4 | if ( listFirst( getController().getColdBoxSetting( "version" ), "." ) >= 8 ) { 5 | setMultiDomainDiscovery( false ); 6 | setBaseUrl( composeRoutingUrl() & "index.cfm/" ); 7 | } else { 8 | setFullRewrites( false ); 9 | } 10 | 11 | // Conventions based routing 12 | route( ":handler/:action?" ).end(); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /tests/specs/integration/ModuleActivationSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | function run() { 4 | describe( "Sample Integration Specs", function() { 5 | it( "can run integration specs with the module activated", function() { 6 | expect( getController().getModuleService().isModuleRegistered( "hyper" ) ).toBeTrue(); 7 | } ); 8 | } ); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /tests/resources/app/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /tests/resources/ModuleIntegrationSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="coldbox.system.testing.BaseTestCase" { 2 | 3 | this.unloadColdBox = true; 4 | 5 | function beforeAll() { 6 | super.beforeAll(); 7 | 8 | getController().getModuleService() 9 | .registerAndActivateModule( "hyper", "testingModuleRoot" ); 10 | } 11 | 12 | /** 13 | * @beforeEach 14 | */ 15 | function setupIntegrationTest() { 16 | setup(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /server-adobe@be.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-adobe@be", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobeBE", 5 | "cfengine":"adobe@be" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "jvm":{ 16 | "heapSize":"1024" 17 | }, 18 | "openBrowser":"false", 19 | "scripts" : { 20 | "onServerInstall":"cfpm install zip,debugger" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server-adobe@2023.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-adobe@2023", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2023", 5 | "cfengine":"adobe@2023" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "jvm":{ 16 | "heapSize":"1024" 17 | }, 18 | "openBrowser":"false", 19 | "scripts":{ 20 | "onServerInstall":"cfpm install zip,debugger" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server-adobe@2021.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-adobe@2021", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2021", 5 | "cfengine":"adobe@2021" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "jvm":{ 16 | "heapSize":"1024", 17 | "javaVersion":"openjdk11_jre" 18 | }, 19 | "openBrowser":"false", 20 | "scripts":{ 21 | "onServerInstall":"cfpm install zip,debugger" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server-adobe@2025.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-adobe@2025", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2025", 5 | "cfengine":"adobe@2025" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "jvm":{ 16 | "heapSize":"1024", 17 | "javaVersion":"openjdk21_jre" 18 | }, 19 | "openBrowser":"false", 20 | "scripts":{ 21 | "onServerInstall":"cfpm install zip,debugger" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/resources/app/.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain -------------------------------------------------------------------------------- /tests/resources/app/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | User-agent: Slurp 3 | Crawl-delay: 100 4 | Disallow: 5 | 6 | User-agent: gsa-crawler-www 7 | Crawl-delay: 100 8 | 9 | User-agent: Googlebot 10 | Crawl-delay: 100 11 | 12 | User-agent: Mediapartners-Google 13 | Disallow: 14 | 15 | User-agent: Yahoo-NewsCrawler 16 | Disallow: 17 | 18 | User-Agent: msnbot 19 | Crawl-delay: 100 20 | Disallow: 21 | 22 | User-Agent: * 23 | Disallow: /config/ 24 | Disallow: /handlers/ 25 | Disallow: /includes/ 26 | Disallow: /interceptors/ 27 | Disallow: /layouts/ 28 | Disallow: /logs/ 29 | Disallow: /models/ 30 | Disallow: /modules/ 31 | Disallow: /views/ 32 | Allow: / -------------------------------------------------------------------------------- /server-boxlang@1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-boxlang@1", 3 | "app":{ 4 | "serverHomeDirectory":".engine/boxlang", 5 | "cfengine":"boxlang@^1.0.0" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":true 13 | } 14 | }, 15 | "JVM":{ 16 | "heapSize":"1024", 17 | "javaVersion":"openjdk21_jre", 18 | "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888" 19 | }, 20 | "openBrowser":false, 21 | "env":{}, 22 | "scripts":{ 23 | "onServerInitialInstall":"install bx-esapi" 24 | } 25 | } -------------------------------------------------------------------------------- /server-boxlang@be.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-boxlang@be", 3 | "app":{ 4 | "serverHomeDirectory":".engine/boxlangBE", 5 | "cfengine":"boxlang@be" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites": { 12 | "enable": true 13 | } 14 | }, 15 | "JVM": { 16 | "heapSize": "1024", 17 | "javaVersion": "openjdk21_jre", 18 | "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888" 19 | }, 20 | "openBrowser": false, 21 | "env": {}, 22 | "scripts": { 23 | "onServerInitialInstall": "install bx-esapi" 24 | } 25 | } -------------------------------------------------------------------------------- /server-boxlang-cfml@1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper-boxlang-cfml@1", 3 | "app":{ 4 | "serverHomeDirectory":".engine/boxlangCFML", 5 | "cfengine":"boxlang@^1.0.0" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"8500" 10 | }, 11 | "rewrites":{ 12 | "enable":true 13 | } 14 | }, 15 | "JVM":{ 16 | "heapSize":"1024", 17 | "javaVersion":"openjdk21_jre", 18 | "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888" 19 | }, 20 | "openBrowser":false, 21 | "env":{}, 22 | "scripts":{ 23 | "onServerInitialInstall":"install bx-esapi,bx-compat-cfml" 24 | } 25 | } -------------------------------------------------------------------------------- /tests/resources/app/tests/runner.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/runner.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/resources/app/readme.md: -------------------------------------------------------------------------------- 1 | # Advanced Template 2 | 3 | An advanced template with all the bells and whistles in script format 4 | 5 | ## License 6 | Apache License, Version 2.0. 7 | 8 | ## Important Links 9 | 10 | Source Code 11 | - https://github.com/coldbox-templates/advanced-script 12 | 13 | ## Quick Installation 14 | 15 | Each application templates contains a `box.json` so it can leverage [CommandBox](http://www.ortussolutions.com/products/commandbox) for its dependencies. 16 | Just go into each template directory and type: 17 | 18 | ``` 19 | box install 20 | ``` 21 | 22 | This will setup all the needed dependencies for each application template. You can then type: 23 | 24 | ``` 25 | box server start 26 | ``` 27 | 28 | And run the application. 29 | 30 | --- 31 | 32 | ###THE DAILY BREAD 33 | > "I am the way, and the truth, and the life; no one comes to the Father, but by me (JESUS)" Jn 14:1-12 -------------------------------------------------------------------------------- /tests/resources/app/tests/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | ******************************************************************************** 3 | Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 4 | www.ortussolutions.com 5 | ******************************************************************************** 6 | */ 7 | component{ 8 | 9 | // APPLICATION CFC PROPERTIES 10 | this.name = "ColdBoxTestingSuite" & hash(getCurrentTemplatePath()); 11 | this.sessionManagement = true; 12 | this.sessionTimeout = createTimeSpan( 0, 0, 15, 0 ); 13 | this.applicationTimeout = createTimeSpan( 0, 0, 15, 0 ); 14 | this.setClientCookies = true; 15 | 16 | // Create testing mapping 17 | this.mappings[ "/tests" ] = getDirectoryFromPath( getCurrentTemplatePath() ); 18 | // Map back to its root 19 | rootPath = REReplaceNoCase( this.mappings[ "/tests" ], "tests(\\|/)", "" ); 20 | this.mappings["/root"] = rootPath; 21 | 22 | } -------------------------------------------------------------------------------- /models/HyperHttpClientInterface.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Responsible for executing the HyperRequest and mapping it to a HyperResponse 3 | */ 4 | interface displayname="HyperHttpClientInterface" { 5 | 6 | /** 7 | * Execute the HyperRequest and map it to a HyperResponse. 8 | * 9 | * @req The HyperRequest to execute. 10 | * 11 | * @returns A HyperResponse of the executed request. 12 | */ 13 | public HyperResponse function send( required HyperRequest req ); 14 | 15 | /** 16 | * Return a struct of information showing how the client will execute the HyperRequest. 17 | * This will be used by a developer to debug any differences between the generated 18 | * request values and the expected request values. 19 | * 20 | * @req The HyperRequest to debug. 21 | * 22 | * @returns A struct of information detailing how the client would execute the HyperRequest. 23 | */ 24 | public struct function debug( required HyperRequest req ); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /tests/specs/unit/HyperBuilderSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "HyperBuilder", function() { 5 | beforeEach( function() { 6 | variables.hyper = new Hyper.models.HyperBuilder(); 7 | } ); 8 | 9 | afterEach( function() { 10 | if ( structKeyExists( variables, "hyper" ) ) { 11 | structDelete( variables, "hyper" ); 12 | } 13 | } ); 14 | 15 | it( "can create a new request", function() { 16 | expect( hyper.new() ).toBeInstanceOf( "HyperRequest" ); 17 | } ); 18 | 19 | it( "passes through all other methods to the HyperRequest", function() { 20 | var req = hyper.setUrl( "https://jsonplaceholder.typicode.com/posts/1" ); 21 | expect( req ).toBeInstanceOf( 22 | "HyperRequest", 23 | "Expected a HyperRequest instance, since HyperBuilder does not have a `setUrl` method." 24 | ); 25 | expect( req.getUrl() ).toBe( "https://jsonplaceholder.typicode.com/posts/1" ); 26 | } ); 27 | } ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tests/resources/app/views/_templates/404.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | The page you requested does not exist 9 | 22 | 23 | 24 | 25 |
26 |

The page you requested does not exist.

27 |

You may have mistyped the address or the page may have moved. Please check your address again.

28 |
29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Eric Peterson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/specs/integration/IncompleteRequestsSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | variables.localEndpoint = "http://#CGI[ "server_name" ]#:#CGI[ "server_port" ]#/tests/resources/app/index.cfm/api"; 4 | 5 | function run() { 6 | describe( "incomplete requests", function() { 7 | beforeEach( function() { 8 | variables.hyper = getInstance( "HyperBuilder@Hyper" ); 9 | } ); 10 | 11 | afterEach( function() { 12 | if ( structKeyExists( variables, "hyper" ) ) { 13 | structDelete( variables, "hyper" ); 14 | } 15 | } ); 16 | 17 | it( "returns a 502 status code for any request that cannot be completed", function() { 18 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 19 | expect( res.getStatusCode() ).toBe( 502 ); 20 | expect( res.getStatusText() ).toBe( "Bad Gateway" ); 21 | } ); 22 | 23 | it( "returns a 408 status code for any request that times out", function() { 24 | var res = hyper.setTimeout( 1 ).get( "#localEndpoint#/sleep/delay/5" ); 25 | expect( res.getStatusCode() ).toBe( 408 ); 26 | expect( res.getStatusText() ).toBe( "Request Timeout" ); 27 | } ); 28 | } ); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /tests/Application.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | this.name = "ColdBoxTestingSuite" & hash(getCurrentTemplatePath()); 4 | this.sessionManagement = true; 5 | this.setClientCookies = true; 6 | this.sessionTimeout = createTimeSpan( 0, 0, 15, 0 ); 7 | this.applicationTimeout = createTimeSpan( 0, 0, 15, 0 ); 8 | 9 | testsPath = getDirectoryFromPath( getCurrentTemplatePath() ); 10 | this.mappings[ "/tests" ] = testsPath; 11 | rootPath = REReplaceNoCase( this.mappings[ "/tests" ], "tests(\\|/)", "" ); 12 | this.mappings[ "/root" ] = rootPath; 13 | this.mappings[ "/hyper" ] = rootPath; 14 | this.mappings[ "/globber" ] = rootPath & "modules/globber"; 15 | testingModuleRootMapping = listDeleteAt( rootPath, listLen( rootPath, '\/' ), "\/" ); 16 | if ( left( testingModuleRootMapping, 1 ) != "/" ) { 17 | testingModuleRootMapping = "/" & testingModuleRootMapping; 18 | } 19 | this.mappings[ "/testingModuleRoot" ] = testingModuleRootMapping; 20 | this.mappings[ "/app" ] = testsPath & "resources/app"; 21 | this.mappings[ "/coldbox" ] = testsPath & "resources/app/coldbox"; 22 | this.mappings[ "/testbox" ] = rootPath & "/testbox"; 23 | 24 | // function onRequestStart() { 25 | // applicationStop(); 26 | // abort; 27 | // } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/resources/app/config/WireBox.cfc: -------------------------------------------------------------------------------- 1 | component extends="coldbox.system.ioc.config.Binder" { 2 | 3 | /** 4 | * Configure WireBox, that's it! 5 | */ 6 | function configure() { 7 | // The WireBox configuration structure DSL 8 | wireBox = { 9 | // Scope registration, automatically register a wirebox injector instance on any CF scope 10 | // By default it registeres itself on application scope 11 | "scopeRegistration" : { 12 | "enabled" : true, 13 | "scope" : "application", // server, cluster, session, application 14 | "key" : "wireBox" 15 | }, 16 | // DSL Namespace registrations 17 | "customDSL" : {}, 18 | // Custom Storage Scopes 19 | "customScopes" : {}, 20 | // Package scan locations 21 | "scanLocations" : [], 22 | // Stop Recursions 23 | "stopRecursions" : [], 24 | // Parent Injector to assign to the configured injector, this must be an object reference 25 | "parentInjector" : "", 26 | // Register all event listeners here, they are created in the specified order 27 | "listeners" : [] 28 | }; 29 | 30 | // Map Bindings below 31 | map( "SWAPIClient" ) 32 | .to( "hyper.models.HyperBuilder" ) 33 | .asSingleton() 34 | .initWith( baseUrl = "https://swapi.dev/api" ); 35 | 36 | map( "JSONPlaceholderClient" ) 37 | .to( "hyper.models.HyperBuilder" ) 38 | .asSingleton() 39 | .initWith( baseUrl = "https://jsonplaceholder.typicode.com" ); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /tests/specs/integration/BuilderPassThroughSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | function run() { 4 | describe( "Builder pass through requests", function() { 5 | beforeEach( function() { 6 | variables.hyper = getInstance( "HyperBuilder@Hyper" ); 7 | } ); 8 | 9 | afterEach( function() { 10 | if ( structKeyExists( variables, "hyper" ) ) { 11 | structDelete( variables, "hyper" ); 12 | } 13 | } ); 14 | 15 | it( "passes through all other methods to the HyperRequest", function() { 16 | var res = hyper.setUrl( "https://jsonplaceholder.typicode.com/posts/1" ).get(); 17 | expect( res ).toBeInstanceOf( "HyperResponse", "A HyperResponse object should have been returned." ); 18 | var data = res.json(); 19 | expect( data ).toBeStruct( "Expected to deserialize JSON data from the response." ); 20 | expect( data ).toBe( 21 | deserializeJSON( 22 | "{ 23 | ""userId"": 1, 24 | ""id"": 1, 25 | ""title"": ""sunt aut facere repellat provident occaecati excepturi optio reprehenderit"", 26 | ""body"": ""quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"" 27 | }" 28 | ) 29 | ); 30 | } ); 31 | } ); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /tests/specs/integration/FileUploadsSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | variables.localEndpoint = "http://#CGI[ "server_name" ]#:#CGI[ "server_port" ]#/tests/resources/app/index.cfm/api"; 4 | 5 | function run() { 6 | describe( "File Uploads requests", function() { 7 | beforeEach( function() { 8 | variables.hyper = getInstance( "HyperBuilder@Hyper" ); 9 | } ); 10 | 11 | afterEach( function() { 12 | if ( structKeyExists( variables, "hyper" ) ) { 13 | structDelete( variables, "hyper" ); 14 | } 15 | } ); 16 | 17 | it( "can attach files", function() { 18 | if ( isBoxLang() ) { 19 | return skip( "File uploads work but have a bug that prevents it from passing on the first time." ); 20 | } 21 | 22 | var res = hyper 23 | .attach( 24 | "smallPhoto", 25 | expandPath( "/tests/resources/chuck_norris.jpg" ), 26 | "image/jpeg" 27 | ) 28 | .attach( "largePhoto", expandPath( "/tests/resources/chuck_norris.jpg" ) ) 29 | .post( 30 | "#localEndpoint#/photos", 31 | { "description" : "Chuck Norris doesn't need two different photos." } 32 | ); 33 | expect( res.getStatusCode() ).toBe( 201, res.getData() ); 34 | var json = res.json(); 35 | expect( json.smallPhoto ).toBe( "chuck_norris.jpg" ); 36 | expect( json.largePhoto ).toBe( "chuck_norris.jpg" ); 37 | expect( json.description ).toBe( "Chuck Norris doesn't need two different photos." ); 38 | } ); 39 | } ); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /tests/resources/app/handlers/Main.cfc: -------------------------------------------------------------------------------- 1 | component extends="coldbox.system.EventHandler" { 2 | 3 | /** 4 | * Default Action 5 | */ 6 | function index( event, rc, prc ) { 7 | prc.welcomeMessage = "Welcome to ColdBox!"; 8 | event.setView( "main/index" ); 9 | } 10 | 11 | /** 12 | * Produce some restfulf data 13 | */ 14 | function data( event, rc, prc ) { 15 | return [ 16 | { "id" : createUUID(), name : "Luis" }, 17 | { "id" : createUUID(), name : "JOe" }, 18 | { "id" : createUUID(), name : "Bob" }, 19 | { "id" : createUUID(), name : "Darth" } 20 | ]; 21 | } 22 | 23 | /** 24 | * Relocation example 25 | */ 26 | function doSomething( event, rc, prc ) { 27 | relocate( "main.index" ); 28 | } 29 | 30 | /************************************** IMPLICIT ACTIONS *********************************************/ 31 | 32 | function onAppInit( event, rc, prc ) { 33 | } 34 | 35 | function onRequestStart( event, rc, prc ) { 36 | } 37 | 38 | function onRequestEnd( event, rc, prc ) { 39 | } 40 | 41 | function onSessionStart( event, rc, prc ) { 42 | } 43 | 44 | function onSessionEnd( event, rc, prc ) { 45 | var sessionScope = event.getValue( "sessionReference" ); 46 | var applicationScope = event.getValue( "applicationReference" ); 47 | } 48 | 49 | function onException( event, rc, prc ) { 50 | event.setHTTPHeader( statusCode = 500 ); 51 | // Grab Exception From private request collection, placed by ColdBox Exception Handling 52 | var exception = prc.exception; 53 | // Place exception handler below: 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/resources/app/views/_templates/generic_error.cfm: -------------------------------------------------------------------------------- 1 | 2 |

An Unhandled Exception Occurred

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 38 | 39 | 40 | 41 | 42 |
An unhandled exception has occurred. Please look at the diagnostic information below:
Type#exception.getType()#
Message#exception.getMessage()#
Detail#exception.getDetail()#
Extended Info#exception.getExtendedInfo()#
Message#exception.getMessage()#
Tag Context 31 | 32 | 33 | 34 | #variables.tagCtx['template']# (#variables.tagCtx['line']#)
35 |
36 |
Stack Trace#exception.getStackTrace()#
43 |
44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

hyper

2 |

3 | Available on ForgeBox 4 | Tested With TestBox 5 |

6 | 7 | ## A CFML HTTP Builder 8 | 9 | ### Inspiration 10 | 11 | Hyper was built after coding several API SDK's for various platforms — 12 | [S3SDK](https://github.com/coldbox-modules/s3sdk), 13 | [cbstripe](https://github.com/coldbox-modules/cbox-stripe), and 14 | [cbgithub](https://github.com/elpete/cbgithub), to name a few. I noticed that I 15 | spent a lot of time setting up the plumbing for the requests and a wrapper 16 | around `cfhttp`. Each implementation was mostly the same but slightly different. 17 | It was additionally frustrating because I really only needed to tweak a few 18 | values, usually just the `Authorization` header. It would be nice to create an 19 | HTTP client pre-configured for each of these SDK's. It seemed the perfect fit 20 | for a module. 21 | 22 | ### The problem it solves 23 | 24 | Hyper exists to provide a fluent builder experience for HTTP requests and responses. It also provides a powerful way to create clients, i.e. Builder objects with pre-configured defaults like a base URL or certain headers. 25 | 26 | ### Requirements 27 | 28 | Hyper runs on Adobe ColdFusion 2018+ and Lucee 5+. 29 | 30 | ColdBox is not required, but mappings are provided for ColdBox users automatically. 31 | 32 | ### Documentation 33 | 34 | You can find all of the documentation for Hyper at [https://hyper.ortusbooks.com](https://hyper.ortusbooks.com). -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"hyper", 3 | "version":"8.0.1", 4 | "author":"Eric Peterson ", 5 | "location":"forgeboxStorage", 6 | "homepage":"https://github.com/coldbox-modules/hyper", 7 | "documentation":"https://github.com/coldbox-modules/hyper", 8 | "repository":{ 9 | "type":"git", 10 | "URL":"https://github.com/coldbox-modules/hyper" 11 | }, 12 | "license":[ 13 | { 14 | "type":"MIT", 15 | "URL":"https://github.com/coldbox-modules/hyper/LICENSE" 16 | } 17 | ], 18 | "bugs":"https://github.com/coldbox-modules/hyper/issues", 19 | "slug":"hyper", 20 | "shortDescription":"A CFML HTTP Builder", 21 | "description":"A CFML HTTP Builder", 22 | "type":"modules", 23 | "dependencies":{ 24 | "globber":"^3.1.5" 25 | }, 26 | "devDependencies":{ 27 | "testbox":"^6.0.0", 28 | "coldbox":"^8.0.0" 29 | }, 30 | "installPaths":{ 31 | "testbox":"testbox/", 32 | "coldbox":"tests/resources/app/coldbox/", 33 | "globber":"modules/globber/" 34 | }, 35 | "scripts":{ 36 | "format":"cfformat run ModuleConfig.cfc,models/**/*.cfc,tests/specs/**/*.cfc,tests/resources/app/handlers/**/*.cfc,tests/resources/app/config/**/*.cfc --overwrite", 37 | "format:check":"cfformat check ModuleConfig.cfc,models/**/*.cfc,tests/specs/**/*.cfc,tests/resources/app/handlers/**/*.cfc,tests/resources/app/config/**/*.cfc --verbose", 38 | "format:watch":"cfformat watch ModuleConfig.cfc,models/**/*.cfc,tests/specs/**/*.cfc,tests/resources/app/handlers/**/*.cfc,tests/resources/app/config/**/*.cfc", 39 | "bx-modules:install":"install bx-compat-cfml@be,bx-esapi" 40 | }, 41 | "ignore":[ 42 | "**/.*", 43 | "test", 44 | "tests" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /tests/resources/app/config/CacheBox.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | /** 4 | * Configure CacheBox for ColdBox Application Operation 5 | */ 6 | function configure() { 7 | // The CacheBox configuration structure DSL 8 | cacheBox = { 9 | // LogBox config already in coldbox app, not needed 10 | // logBoxConfig = "coldbox.system.web.config.LogBox", 11 | 12 | // The defaultCache has an implicit name "default" which is a reserved cache name 13 | // It also has a default provider of cachebox which cannot be changed. 14 | // All timeouts are in minutes 15 | defaultCache : { 16 | objectDefaultTimeout : 120, // two hours default 17 | objectDefaultLastAccessTimeout : 30, // 30 minutes idle time 18 | useLastAccessTimeouts : true, 19 | reapFrequency : 5, 20 | freeMemoryPercentageThreshold : 0, 21 | evictionPolicy : "LRU", 22 | evictCount : 1, 23 | maxObjects : 300, 24 | objectStore : "ConcurrentStore", // guaranteed objects 25 | coldboxEnabled : true 26 | }, 27 | // Register all the custom named caches you like here 28 | caches : { 29 | // Named cache for all coldbox event and view template caching 30 | template : { 31 | provider : "coldbox.system.cache.providers.CacheBoxColdBoxProvider", 32 | properties : { 33 | objectDefaultTimeout : 120, 34 | objectDefaultLastAccessTimeout : 30, 35 | useLastAccessTimeouts : true, 36 | freeMemoryPercentageThreshold : 0, 37 | reapFrequency : 5, 38 | evictionPolicy : "LRU", 39 | evictCount : 2, 40 | maxObjects : 300, 41 | objectStore : "ConcurrentSoftReferenceStore" // memory sensitive 42 | } 43 | } 44 | } 45 | }; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/resources/app/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 3 | * www.ortussolutions.com 4 | * --- 5 | */ 6 | component{ 7 | // Application properties 8 | this.name = hash( getCurrentTemplatePath() ); 9 | this.sessionManagement = true; 10 | this.sessionTimeout = createTimeSpan(0,0,30,0); 11 | this.setClientCookies = true; 12 | 13 | appPath = getDirectoryFromPath( getCurrentTemplatePath() ); 14 | this.mappings[ "/app" ] = appPath; 15 | rootPath = replaceNoCase( this.mappings[ "/app" ], "/tests/resources/app", "" ); 16 | this.mappings[ "/root" ] = rootPath; 17 | this.mappings[ "/testingModuleRoot" ] = listDeleteAt( rootPath, listLen( rootPath, '\/' ), "\/" ); 18 | this.mappings[ "/coldbox" ] = appPath & "coldbox"; 19 | 20 | // COLDBOX STATIC PROPERTY, DO NOT CHANGE UNLESS THIS IS NOT THE ROOT OF YOUR COLDBOX APP 21 | COLDBOX_APP_ROOT_PATH = appPath; 22 | // The web server mapping to this application. Used for remote purposes or static purposes 23 | COLDBOX_APP_MAPPING = ""; 24 | // COLDBOX PROPERTIES 25 | COLDBOX_CONFIG_FILE = ""; 26 | // COLDBOX APPLICATION KEY OVERRIDE 27 | COLDBOX_APP_KEY = ""; 28 | 29 | // application start 30 | public boolean function onApplicationStart(){ 31 | application.cbBootstrap = new coldbox.system.Bootstrap( COLDBOX_CONFIG_FILE, COLDBOX_APP_ROOT_PATH, COLDBOX_APP_KEY, COLDBOX_APP_MAPPING ); 32 | application.cbBootstrap.loadColdbox(); 33 | return true; 34 | } 35 | 36 | // application end 37 | public void function onApplicationEnd( struct appScope ){ 38 | arguments.appScope.cbBootstrap.onApplicationEnd( arguments.appScope ); 39 | } 40 | 41 | // request start 42 | public boolean function onRequestStart( string targetPage ){ 43 | // Process ColdBox Request 44 | application.cbBootstrap.onRequestStart( arguments.targetPage ); 45 | 46 | return true; 47 | } 48 | 49 | public void function onSessionStart(){ 50 | application.cbBootStrap.onSessionStart(); 51 | } 52 | 53 | public void function onSessionEnd( struct sessionScope, struct appScope ){ 54 | arguments.appScope.cbBootStrap.onSessionEnd( argumentCollection=arguments ); 55 | } 56 | 57 | public boolean function onMissingTemplate( template ){ 58 | return application.cbBootstrap.onMissingTemplate( argumentCollection=arguments ); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /tests/specs/integration/RedirectsSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | variables.localEndpoint = "http://#CGI[ "server_name" ]#:#CGI[ "server_port" ]#/tests/resources/app/index.cfm/api"; 4 | 5 | function run() { 6 | describe( "GET requests", function() { 7 | beforeEach( function() { 8 | variables.hyper = getInstance( "HyperBuilder@Hyper" ); 9 | } ); 10 | 11 | afterEach( function() { 12 | if ( structKeyExists( variables, "hyper" ) ) { 13 | structDelete( variables, "hyper" ); 14 | } 15 | } ); 16 | 17 | it( "follows redirects by default", function() { 18 | var res = hyper.get( localEndpoint & "/redirect" ); 19 | expect( res.getStatusCode() ).toBe( 200 ); 20 | expect( res.json() ).toBe( { "message" : "Hello, world!" } ); 21 | var referrer = res.getRequest().getReferrer(); 22 | expect( referrer ).notToBeNull(); 23 | expect( referrer.getStatusCode() ).toBe( 302 ); 24 | expect( referrer.getRequest().getUrl() ).toBe( localEndpoint & "/redirect" ); 25 | } ); 26 | 27 | it( "appends the base url of the previous request if the redirect is a partial url", function() { 28 | var res = hyper.get( localEndpoint & "/redirectPartial" ); 29 | expect( res.getStatusCode() ).toBe( 200 ); 30 | expect( res.json() ).toBe( { "message" : "Hello, world!" } ); 31 | var referrer = res.getRequest().getReferrer(); 32 | expect( referrer ).notToBeNull(); 33 | expect( referrer.getStatusCode() ).toBe( 302 ); 34 | expect( referrer.getRequest().getUrl() ).toBe( localEndpoint & "/redirectPartial" ); 35 | } ); 36 | 37 | it( "can set the maximum number of redirects to follow", function() { 38 | var res = hyper.setMaximumRedirects( 2 ).get( localEndpoint & "/redirect?times=3" ); 39 | expect( res.getStatusCode() ).toBe( 302 ); 40 | } ); 41 | 42 | it( "can turn off following redirects by setting max redirects to zero", function() { 43 | var res = hyper.setMaximumRedirects( 0 ).get( localEndpoint & "/redirect" ); 44 | expect( res.getStatusCode() ).toBe( 302 ); 45 | } ); 46 | 47 | it( "has a helper method to not follow redirects", function() { 48 | var res = hyper.withoutRedirecting().get( localEndpoint & "/redirect" ); 49 | expect( res.getStatusCode() ).toBe( 302 ); 50 | } ); 51 | } ); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Cron 2 | 3 | on: 4 | schedule: 5 | - cron: 0 0 * * 1 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | name: Tests 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "boxlang-cfml@1"] 15 | coldbox: ["^6.0.0", "^7.0.0", "^8.0.0"] 16 | experimental: [ false ] 17 | include: 18 | - cfengine: "lucee@be" 19 | coldbox: "^6.0.0" 20 | experimental: true 21 | - cfengine: "lucee@be" 22 | coldbox: "^7.0.0" 23 | experimental: true 24 | - cfengine: "lucee@be" 25 | coldbox: "^8.0.0" 26 | experimental: true 27 | - cfengine: "adobe@2025" 28 | coldbox: "^7.0.0" 29 | experimental: false 30 | - cfengine: "adobe@2025" 31 | coldbox: "^8.0.0" 32 | experimental: false 33 | - cfengine: "adobe@be" 34 | coldbox: "^6.0.0" 35 | experimental: true 36 | - cfengine: "adobe@be" 37 | coldbox: "^7.0.0" 38 | experimental: true 39 | - cfengine: "adobe@be" 40 | coldbox: "^8.0.0" 41 | experimental: true 42 | - cfengine: "boxlang@be" 43 | coldbox: "^8.0.0" 44 | experimental: true 45 | - cfengine: "boxlang@1" 46 | coldbox: "^8.0.0" 47 | experimental: false 48 | steps: 49 | - name: Checkout Repository 50 | uses: actions/checkout@v3.2.0 51 | 52 | - name: Setup Java JDK 53 | uses: actions/setup-java@v3.9.0 54 | with: 55 | distribution: 'zulu' 56 | java-version: 21 57 | 58 | - name: Set Up CommandBox 59 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 60 | with: 61 | install: commandbox-boxlang,commandbox-cfconfig 62 | 63 | - name: Install dependencies 64 | run: | 65 | box install 66 | box install coldbox@${{ matrix.coldbox }} --noSave 67 | 68 | - name: Start server 69 | run: | 70 | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug 71 | curl http://127.0.0.1:8500 72 | 73 | - name: Run TestBox Tests 74 | continue-on-error: ${{ matrix.experimental }} 75 | run: box testbox run -------------------------------------------------------------------------------- /tests/resources/app/handlers/api.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function index( event, rc, prc ) { 4 | return event.renderData( type = "json", data = { "message" : "Hello, world!" } ); 5 | } 6 | 7 | function redirect( event, rc, prc ) { 8 | param rc.times = 0; 9 | if ( rc.times == 0 ) { 10 | relocate( "api.index" ); 11 | } else { 12 | relocate( event = "api.redirect", queryString = "times=#rc.times - 1#" ); 13 | } 14 | } 15 | 16 | function redirectPartial( event, rc, prc ) { 17 | relocate( url = "/tests/resources/app/index.cfm/api/index" ); 18 | } 19 | 20 | function create( event, rc, prc ) { 21 | var data = { "id" : 101 }; 22 | var content = event.getHTTPContent(); 23 | if ( isJSON( content ) ) { 24 | structAppend( data, deserializeJSON( content ) ); 25 | } 26 | if ( event.valueExists( "fieldNames" ) ) { 27 | for ( var field in listToArray( rc.fieldNames ) ) { 28 | data[ field ] = rc[ field ]; 29 | } 30 | } 31 | return event.renderData( 32 | type = "json", 33 | statusCode = 201, 34 | data = data 35 | ); 36 | } 37 | 38 | function photos( event, rc, prc ) { 39 | if ( !event.valueExists( "smallPhoto" ) ) { 40 | return event.renderData( 41 | type = "text", 42 | statusCode = 422, 43 | data = "A `smallPhoto` is required." 44 | ); 45 | } 46 | 47 | if ( !event.valueExists( "largePhoto" ) ) { 48 | return event.renderData( 49 | type = "text", 50 | statusCode = 422, 51 | data = "A `largePhoto` is required." 52 | ); 53 | } 54 | 55 | var smallPhoto = fileUpload( 56 | getTempDirectory(), 57 | "smallPhoto", 58 | "*", 59 | "overwrite" 60 | ); 61 | 62 | var largePhoto = fileUpload( 63 | getTempDirectory(), 64 | "largePhoto", 65 | "*", 66 | "overwrite" 67 | ); 68 | 69 | return event.renderData( 70 | type = "json", 71 | statusCode = 201, 72 | data = { 73 | "id" : 777, 74 | "smallPhoto" : smallPhoto.serverFile.reFind( "[/\\]+" ) > 0 ? smallPhoto.serverFileName : smallPhoto.serverFile, 75 | "largePhoto" : largePhoto.serverFile.reFind( "[/\\]+" ) > 0 ? largePhoto.serverFileName : largePhoto.serverFile, 76 | "description" : rc.description 77 | } 78 | ); 79 | } 80 | 81 | function sleep( event, rc, prc ) { 82 | param rc.delay = 0; 83 | sleep( rc.delay * 1000 ); 84 | return event.renderData( 85 | type = "plain", 86 | statusCode = 200, 87 | data = "Slept for #rc.delay# seconds." 88 | ); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /models/FakeHyperResponse.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * A fake HyperResponse where the properties can be set. 3 | */ 4 | component accessors="true" extends="HyperResponse" { 5 | 6 | /** 7 | * Unique response ID representing this response. 8 | */ 9 | property name="responseID"; 10 | 11 | /** 12 | * The status code for the response. 13 | */ 14 | property name="statusCode" default="200"; 15 | 16 | /** 17 | * The status text for the response. 18 | */ 19 | property name="statusText" default="OK"; 20 | 21 | /** 22 | * The data for the response. 23 | */ 24 | property name="data" default=""; 25 | 26 | /** 27 | * The HyperRequest instance associated with this response. 28 | */ 29 | property name="request"; 30 | 31 | /** 32 | * The charset value for the response. 33 | */ 34 | property name="charset" default="UTF-8"; 35 | 36 | /** 37 | * The charset value for the response. 38 | */ 39 | property name="headers" type="struct"; 40 | 41 | /** 42 | * The timestamp for when this response was received. 43 | */ 44 | property name="timestamp"; 45 | 46 | /** 47 | * The execution time of the request, in milliseconds. 48 | */ 49 | property name="executionTime"; 50 | 51 | /** 52 | * Create a new FakeHyperResponse. 53 | * 54 | * @originalRequest The HyperRequest associated with this response. 55 | * @executionTime The execution time of the request, in milliseconds. 56 | * @charset The response charset. Default: UTF-8 57 | * @statusCode The response status code. Default: 200. 58 | * @headers The response headers. Default: {}. 59 | * @data The response data. Default: "". 60 | * @timestamp The timestamp for when this response was received. Default: `now()`. 61 | * 62 | * @returns A new HyperResponse instance. 63 | */ 64 | public HyperResponse function init( 65 | required HyperRequest originalRequest, 66 | numeric executionTime = 0, 67 | string charset = "UTF-8", 68 | numeric statusCode = 200, 69 | string statusText = "OK", 70 | struct headers = {}, 71 | any data = "", 72 | timestamp = now() 73 | ) { 74 | variables.responseID = createUUID(); 75 | variables.request = arguments.originalRequest; 76 | variables.charset = arguments.charset; 77 | variables.statusCode = arguments.statusCode; 78 | variables.statusText = arguments.statusText; 79 | variables.headers = arguments.headers; 80 | variables.data = arguments.data; 81 | variables.timestamp = arguments.timestamp; 82 | variables.executionTime = arguments.executionTime; 83 | return this; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /models/TestBoxMatchers.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Optional matchers for TestBox when faking requests. 3 | */ 4 | component { 5 | 6 | /** 7 | * Expects a certain request to have been sent. 8 | * The HyperBuilder instance should be passed in to `expect`. 9 | * The first argument should be the predicate function to determine if the request matches. 10 | * An optional message can be passed as the second argument. 11 | */ 12 | function toHaveSentRequest( expectation, args = {} ) { 13 | var hyper = expectation.actual; 14 | if ( structCount( args ) >= 2 ) { 15 | expectation.message = args[ 2 ]; 16 | } else { 17 | expectation.message = expectation.isNot ? "Expected to not find a request that matched the callback parameters but did." : "Expected to find a request that matched the callback parameters but did not."; 18 | } 19 | var sent = hyper.wasRequestSent( args[ 1 ] ); 20 | return expectation.isNot ? !sent : sent; 21 | } 22 | 23 | /** 24 | * Expects a certain number of requests to have been sent. 25 | * The HyperBuilder instance should be passed in to `expect`. 26 | * The first argument should be the number of requests expected to have been sent. 27 | * An optional message can be passed as the second argument. 28 | */ 29 | function toHaveSentCount( expectation, args = {} ) { 30 | var hyper = expectation.actual; 31 | var actualCount = hyper.getFakeRequestCount(); 32 | var expectedCount = args[ 1 ]; 33 | var messagePlural = expectedCount == 1 ? "request" : "requests"; 34 | if ( structCount( args ) >= 2 ) { 35 | expectation.message = args[ 2 ]; 36 | } else { 37 | expectation.message = expectation.isNot ? "Expected not to have sent exactly #expectedCount# #messagePlural# but did." : "Expected to have sent exactly #expectedCount# #messagePlural# but sent #actualCount#."; 38 | } 39 | return expectation.isNot ? actualCount != expectedCount : actualCount == expectedCount; 40 | } 41 | 42 | /** 43 | * Expects no requests to have been sent. 44 | * The HyperBuilder instance should be passed in to `expect`. 45 | * An optional message can be passed as the first argument. 46 | */ 47 | function toHaveSentNothing( expectation, args = {} ) { 48 | var hyper = expectation.actual; 49 | var actualCount = hyper.getFakeRequestCount(); 50 | if ( structCount( args ) >= 1 ) { 51 | expectation.message = args[ 1 ]; 52 | } else { 53 | expectation.message = expectation.isNot ? "Expected to have sent any number of requests but have sent nothing." : "Expected to have sent no requests but have sent #actualCount#."; 54 | } 55 | return expectation.isNot ? actualCount != 0 : actualCount == 0; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /.cfformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "array.empty_padding": false, 3 | "array.padding": true, 4 | "array.multiline.min_length": 40, 5 | "array.multiline.element_count": 2, 6 | "array.multiline.leading_comma.padding": true, 7 | "array.multiline.leading_comma": false, 8 | "alignment.consecutive.assignments": true, 9 | "alignment.consecutive.properties": true, 10 | "alignment.consecutive.params": true, 11 | "brackets.padding": true, 12 | "comment.asterisks": "align", 13 | "binary_operators.padding": true, 14 | "for_loop_semicolons.padding": true, 15 | "function_call.empty_padding": false, 16 | "function_call.padding": true, 17 | "function_call.multiline.leading_comma.padding": true, 18 | "function_call.casing.builtin": "cfdocs", 19 | "function_call.casing.userdefined": "camel", 20 | "function_call.multiline.element_count": 3, 21 | "function_call.multiline.leading_comma": false, 22 | "function_call.multiline.min_length": 40, 23 | "function_declaration.padding": true, 24 | "function_declaration.empty_padding": false, 25 | "function_declaration.multiline.leading_comma": false, 26 | "function_declaration.multiline.leading_comma.padding": true, 27 | "function_declaration.multiline.element_count": 3, 28 | "function_declaration.multiline.min_length": 40, 29 | "function_declaration.group_to_block_spacing": "spaced", 30 | "function_anonymous.empty_padding": false, 31 | "function_anonymous.group_to_block_spacing": "spaced", 32 | "function_anonymous.multiline.element_count": 3, 33 | "function_anonymous.multiline.leading_comma": false, 34 | "function_anonymous.multiline.leading_comma.padding": true, 35 | "function_anonymous.multiline.min_length": 40, 36 | "function_anonymous.padding": true, 37 | "indent_size": 4, 38 | "keywords.block_to_keyword_spacing": "spaced", 39 | "keywords.group_to_block_spacing": "spaced", 40 | "keywords.padding_inside_group": true, 41 | "keywords.spacing_to_block": "spaced", 42 | "keywords.spacing_to_group": true, 43 | "keywords.empty_group_spacing": false, 44 | "max_columns": 120, 45 | "metadata.multiline.element_count": 3, 46 | "metadata.multiline.min_length": 40, 47 | "newline": "\n", 48 | "property.multiline.element_count": 3, 49 | "property.multiline.min_length": 40, 50 | "parentheses.padding": true, 51 | "strings.quote": "double", 52 | "strings.convertNestedQuotes": false, 53 | "strings.attributes.quote": "double", 54 | "struct.separator": " : ", 55 | "struct.padding": true, 56 | "struct.empty_padding": false, 57 | "struct.multiline.leading_comma": false, 58 | "struct.multiline.leading_comma.padding": true, 59 | "struct.multiline.element_count": 2, 60 | "struct.multiline.min_length": 40, 61 | "tab_indent": true 62 | } 63 | -------------------------------------------------------------------------------- /tests/specs/integration/PostSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | variables.localEndpoint = "http://#CGI[ "server_name" ]#:#CGI[ "server_port" ]#/tests/resources/app/index.cfm/api"; 4 | 5 | function run() { 6 | describe( "POST requests", function() { 7 | beforeEach( function() { 8 | variables.hyper = getInstance( "HyperBuilder@Hyper" ); 9 | } ); 10 | 11 | afterEach( function() { 12 | if ( structKeyExists( variables, "hyper" ) ) { 13 | structDelete( variables, "hyper" ); 14 | } 15 | } ); 16 | 17 | it( "can make a POST request", function() { 18 | var res = hyper.post( 19 | "#localEndpoint#/create", 20 | { 21 | "title" : "Example Title", 22 | "body" : "This is my post body" 23 | } 24 | ); 25 | expect( res.getStatusCode() ).toBe( 201 ); 26 | expect( res.json() ).toBe( { 27 | "id" : 101, // this is always the id returned 28 | "title" : "Example Title", 29 | "body" : "This is my post body" 30 | } ); 31 | } ); 32 | 33 | it( "can set the fields the long form way", function() { 34 | var res = hyper 35 | .setUrl( "#localEndpoint#/create" ) 36 | .setBody( { 37 | "title" : "Example Title", 38 | "body" : "This is my post body" 39 | } ) 40 | .post(); 41 | expect( res.getStatusCode() ).toBe( 201 ); 42 | expect( res.json() ).toBe( { 43 | "id" : 101, // this is always the id returned 44 | "title" : "Example Title", 45 | "body" : "This is my post body" 46 | } ); 47 | } ); 48 | 49 | it( "automatically serializes complex values", function() { 50 | var res = hyper.post( 51 | "#localEndpoint#/create", 52 | { 53 | "title" : "Example Title", 54 | "body" : "This is my post body" 55 | } 56 | ); 57 | expect( res.getStatusCode() ).toBe( 201 ); 58 | expect( res.json() ).toBe( { 59 | "id" : 101, // this is always the id returned 60 | "title" : "Example Title", 61 | "body" : "This is my post body" 62 | } ); 63 | } ); 64 | 65 | it( "can post the data as form fields when an array is passed for a form field", function() { 66 | var res = hyper 67 | .asFormFields() 68 | .post( 69 | "#localEndpoint#/create", 70 | { 71 | "title" : "Example Title", 72 | "body" : "This is my post body", 73 | "tags" : [ "tag-a", "tag-b" ] 74 | } 75 | ); 76 | expect( res.getStatusCode() ).toBe( 201 ); 77 | expect( res.json() ).toBe( { 78 | "id" : 101, // this is always the id returned 79 | "title" : "Example Title", 80 | "body" : "This is my post body", 81 | "tags" : "tag-a,tag-b" 82 | } ); 83 | } ); 84 | } ); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /tests/specs/integration/AsyncSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | function beforeAll() { 4 | super.beforeAll(); 5 | variables.hyper = getInstance( "HyperBuilder@hyper" ); 6 | variables.asyncManager = getInstance( "box:asyncManager" ); 7 | } 8 | 9 | function run() { 10 | describe( "async requests", function() { 11 | it( "can send a request asynchronously", function() { 12 | var future = hyper.setUrl( "https://jsonplaceholder.typicode.com/posts/1" ).sendAsync(); 13 | expect( future ).toBeInstanceOf( "Future", "A Future object should have been returned." ); 14 | var res = future.get(); 15 | expect( future.isDone() ).toBeTrue( "Future should be completed." ); 16 | expect( res ).toBeInstanceOf( "HyperResponse", "A HyperResponse object should have been returned." ); 17 | var data = res.json(); 18 | expect( data ).toBeStruct( "Expected to deserialize JSON data from the response." ); 19 | expect( data ).toBe( 20 | deserializeJSON( 21 | "{ 22 | ""userId"": 1, 23 | ""id"" : 1, 24 | ""title"" : ""sunt aut facere repellat provident occaecati excepturi optio reprehenderit"", 25 | ""body"" : ""quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"" 26 | }" 27 | ) 28 | ); 29 | } ); 30 | 31 | it( "can send requests asynchronously without joining", function() { 32 | var requestFinished = false; 33 | hyper 34 | .setUrl( "https://jsonplaceholder.typicode.com/posts/1" ) 35 | .sendAsync() 36 | .then( function( res ) { 37 | requestFinished = true; 38 | } ); 39 | 40 | for ( var i = 0; i <= 3000; i += 100 ) { 41 | if ( requestFinished ) { 42 | return; 43 | } 44 | sleep( 100 ); 45 | } 46 | 47 | fail( "Request did not finish after 3000ms. Aborting." ); 48 | } ); 49 | 50 | it( "can send multiple async requests", function() { 51 | var allFuture = variables.asyncManager.all( [ 52 | hyper.getAsync( "https://jsonplaceholder.typicode.com/posts/1" ), 53 | hyper.getAsync( "https://jsonplaceholder.typicode.com/posts/2" ), 54 | hyper.getAsync( "https://jsonplaceholder.typicode.com/posts/3" ) 55 | ] ); 56 | 57 | expect( allFuture ).toBeInstanceOf( "Future", "A Future object should have been returned." ); 58 | var responseArray = allFuture.get(); 59 | expect( responseArray ).toBeArray(); 60 | expect( responseArray ).toHaveLength( 3 ); 61 | 62 | for ( var i = 1; i <= responseArray.len(); i++ ) { 63 | var res = responseArray[ i ]; 64 | expect( res ).toBeInstanceOf( "HyperResponse", "A HyperResponse object should have been returned." ); 65 | var data = res.json(); 66 | expect( data ).toBeStruct( "Expected to deserialize JSON data from the response." ); 67 | expect( data.id ).toBe( i ); 68 | } 69 | } ); 70 | } ); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /tests/specs/integration/InterceptorsSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | variables.localEndpoint = "http://#CGI[ "server_name" ]#:#CGI[ "server_port" ]#/tests/resources/app/index.cfm/api"; 4 | 5 | function beforeAll() { 6 | super.beforeAll(); 7 | // application.cbController = getController(); 8 | getController().getInterceptorService().registerInterceptor( interceptorObject = this ); 9 | variables.hyper = getInstance( "HyperBuilder@Hyper" ); 10 | } 11 | 12 | function run() { 13 | describe( "interceptors", function() { 14 | beforeEach( function() { 15 | variables.onHyperRequestCalls = []; 16 | variables.onHyperResponseCalls = []; 17 | } ); 18 | 19 | it( "can run an interceptor onHyperRequest", function() { 20 | var res = hyper.get( "https://jsonplaceholder.typicode.com/posts/1" ); 21 | expect( variables.onHyperRequestCalls ).toHaveLength( 1 ); 22 | var interceptorReq = variables.onHyperRequestCalls[ 1 ].request; 23 | expect( interceptorReq ).toBeInstanceOf( "HyperRequest" ); 24 | expect( interceptorReq.getRequestID() ).toBe( res.getRequest().getRequestID() ); 25 | } ); 26 | 27 | it( "can influence the request before it is sent", function() { 28 | var res = hyper.get( "https://jsonplaceholder.typicode.com/posts/1" ); 29 | expect( variables.onHyperRequestCalls ).toHaveLength( 1 ); 30 | var originalParams = res.getRequest().getQueryParams(); 31 | expect( originalParams ).toBeArray(); 32 | expect( originalParams ).toHaveLength( 1 ); 33 | expect( originalParams[ 1 ].name ).toBe( "added_in_interceptor" ); 34 | expect( originalParams[ 1 ].value ).toBe( true ); 35 | } ); 36 | 37 | it( "can run an interceptor onHyperResponse", function() { 38 | var res = hyper.get( "https://jsonplaceholder.typicode.com/posts/1" ); 39 | expect( variables.onHyperResponseCalls ).toHaveLength( 1 ); 40 | var interceptorRes = variables.onHyperResponseCalls[ 1 ].response; 41 | expect( interceptorRes ).toBeInstanceOf( "HyperResponse" ); 42 | expect( interceptorRes.getResponseID() ).toBe( res.getResponseID() ); 43 | } ); 44 | 45 | it( "calls onHyperResponse for each hop in a redirect", function() { 46 | var res = hyper.get( localEndpoint & "/redirect" ); 47 | expect( variables.onHyperResponseCalls ).toHaveLength( 2 ); 48 | var interceptorResA = variables.onHyperResponseCalls[ 1 ].response; 49 | expect( interceptorResA ).toBeInstanceOf( "HyperResponse" ); 50 | var interceptorResB = variables.onHyperResponseCalls[ 2 ].response; 51 | expect( interceptorResB ).toBeInstanceOf( "HyperResponse" ); 52 | expect( interceptorResB.getResponseID() ).toBe( res.getResponseID() ); 53 | } ); 54 | } ); 55 | } 56 | 57 | function onHyperRequest( event, data ) { 58 | arguments.data.request.withQueryParams( { "added_in_interceptor" : "true" } ); 59 | arrayAppend( variables.onHyperRequestCalls, arguments.data ); 60 | } 61 | 62 | function onHyperResponse( event, data ) { 63 | arrayAppend( variables.onHyperResponseCalls, arguments.data ); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /tests/resources/app/config/Coldbox.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | // Configure ColdBox Application 4 | function configure() { 5 | // coldbox directives 6 | coldbox = { 7 | // Application Setup 8 | appName : "Your app name here", 9 | eventName : "event", 10 | // Development Settings 11 | reinitPassword : "", 12 | handlersIndexAutoReload : true, 13 | // Implicit Events 14 | defaultEvent : "", 15 | requestStartHandler : "Main.onRequestStart", 16 | requestEndHandler : "", 17 | applicationStartHandler : "Main.onAppInit", 18 | applicationEndHandler : "", 19 | sessionStartHandler : "", 20 | sessionEndHandler : "", 21 | missingTemplateHandler : "", 22 | // Extension Points 23 | applicationHelper : "includes/helpers/ApplicationHelper.cfm", 24 | viewsHelper : "", 25 | modulesExternalLocation : [ "modules_app" ], 26 | viewsExternalLocation : "", 27 | layoutsExternalLocation : "", 28 | handlersExternalLocation : "", 29 | requestContextDecorator : "", 30 | controllerDecorator : "", 31 | // Error/Exception Handling 32 | invalidHTTPMethodHandler : "", 33 | exceptionHandler : "main.onException", 34 | onInvalidEvent : "", 35 | customErrorTemplate : "", 36 | // Application Aspects 37 | handlerCaching : false, 38 | eventCaching : false, 39 | viewCaching : false, 40 | customErrorTemplate : "/coldbox/system/exceptions/BugReport.cfm" 41 | }; 42 | 43 | // custom settings 44 | settings = {}; 45 | 46 | // environment settings, create a detectEnvironment() method to detect it yourself. 47 | // create a function with the name of the environment so it can be executed if that environment is detected 48 | // the value of the environment is a list of regex patterns to match the cgi.http_host. 49 | environments = { development : "localhost,^127\.0\.0\.1" }; 50 | 51 | // Module Directives 52 | modules = { 53 | // Turn to false in production 54 | autoReload : false, 55 | // An array of modules names to load, empty means all of them 56 | include : [], 57 | // An array of modules names to NOT load, empty means none 58 | exclude : [] 59 | }; 60 | 61 | // LogBox DSL 62 | logBox = { 63 | // Define Appenders 64 | appenders : { coldboxTracer : { class : "coldbox.system.logging.appenders.ConsoleAppender" } }, 65 | // Root Logger 66 | root : { levelmax : "INFO", appenders : "*" }, 67 | // Implicit Level Categories 68 | info : [ "coldbox.system" ] 69 | }; 70 | 71 | // Layout Settings 72 | layoutSettings = { defaultLayout : "", defaultView : "" }; 73 | 74 | // Interceptor Settings 75 | interceptorSettings = { 76 | throwOnInvalidStates : false, 77 | customInterceptionPoints : "" 78 | }; 79 | 80 | // Register interceptors as an array, we need order 81 | interceptors = []; 82 | } 83 | 84 | /** 85 | * Development environment 86 | */ 87 | function development() { 88 | coldbox.customErrorTemplate = "/coldbox/system/exceptions/BugReport.cfm"; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /tests/specs/integration/BuilderDefaultsSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | function run() { 4 | describe( "Builder defaults", function() { 5 | beforeEach( function() { 6 | variables.hyper = getInstance( "HyperBuilder@Hyper" ); 7 | } ); 8 | 9 | afterEach( function() { 10 | if ( structKeyExists( variables, "hyper" ) ) { 11 | structDelete( variables, "hyper" ); 12 | } 13 | } ); 14 | 15 | it( "can be configured with defaults that get passed to every request", function() { 16 | hyper.defaults.setBaseUrl( "https://jsonplaceholder.typicode.com" ); 17 | var req = hyper.new(); 18 | expect( req.getBaseUrl() ).toBe( "https://jsonplaceholder.typicode.com" ); 19 | } ); 20 | 21 | it( "configures the defaults with any passed in parameters that exist on the request", function() { 22 | variables.hyper = new Hyper.models.HyperBuilder( 23 | baseUrl = "https://jsonplaceholder.typicode.com", 24 | headers = { "X-Requested-With" : "XMLHTTPRequest" } 25 | ); 26 | var req = hyper.new(); 27 | expect( req.getBaseUrl() ).toBe( "https://jsonplaceholder.typicode.com" ); 28 | expect( req.getHeader( "X-Requested-With" ) ).toBe( "XMLHTTPRequest" ); 29 | } ); 30 | 31 | it( "can configure builders in the WireBox config file", () => { 32 | var hyper = getInstance( "JSONPlaceholderClient" ); 33 | var res = hyper.get( "/posts/1" ); 34 | expect( res.isOK() ).toBeTrue(); 35 | var data = res.json(); 36 | expect( data ).toBeStruct( "Expected to deserialize JSON data from the response." ); 37 | expect( data ).toBe( 38 | deserializeJSON( 39 | "{ 40 | ""userId"": 1, 41 | ""id"" : 1, 42 | ""title"" : ""sunt aut facere repellat provident occaecati excepturi optio reprehenderit"", 43 | ""body"" : ""quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"" 44 | }" 45 | ) 46 | ); 47 | } ); 48 | 49 | it( "can configure builders using a registerAs syntax", () => { 50 | getInstance( "HyperBuilder@hyper" ) 51 | .setBaseUrl( "https://jsonplaceholder.typicode.com" ) 52 | .withRequestCallback( function( req ) { 53 | req.withHeaders( { "X-Custom-Header" : "foobar" } ); 54 | } ) 55 | .registerAs( "JSONPlaceholderClient2" ); 56 | 57 | var hyper = getInstance( "JSONPlaceholderClient2" ); 58 | var res = hyper.get( "/posts/1" ); 59 | expect( res.isOK() ).toBeTrue(); 60 | var data = res.json(); 61 | expect( data ).toBeStruct( "Expected to deserialize JSON data from the response." ); 62 | expect( data ).toBe( 63 | deserializeJSON( 64 | "{ 65 | ""userId"": 1, 66 | ""id"" : 1, 67 | ""title"" : ""sunt aut facere repellat provident occaecati excepturi optio reprehenderit"", 68 | ""body"" : ""quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"" 69 | }" 70 | ) 71 | ); 72 | } ); 73 | } ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /tests/specs/integration/DebugSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | function beforeAll() { 4 | super.beforeAll(); 5 | variables.hyper = getInstance( "HyperBuilder@hyper" ); 6 | } 7 | 8 | function run() { 9 | describe( "debug spec", function() { 10 | it( "can print out information about the cfhttp call that will be made", function() { 11 | var binaryBody = charsetDecode( "some binary body", "utf-8" ); 12 | var req = hyper 13 | .withHeaders( { 14 | "Accept" : "application/json;odata=verbose", 15 | "Authorization" : "Bearer token", 16 | "Content-Type" : "application/octet-stream" 17 | } ) 18 | .withCookies( { "foo" : "bar" } ) 19 | .setBody( binaryBody ) 20 | .setBodyFormat( "other|binary" ) 21 | .setMethod( "POST" ) 22 | .setUrl( "https://example.com" ); 23 | 24 | var debugReq = req.debug(); 25 | 26 | expect( debugReq ).toBeStruct(); 27 | 28 | expect( debugReq ).toHaveKey( "attributes" ); 29 | expect( debugReq.attributes ).toHaveKey( "url" ); 30 | expect( debugReq.attributes.url ).toBe( "https://example.com" ); 31 | expect( debugReq.attributes ).toHaveKey( "method" ); 32 | expect( debugReq.attributes.method ).toBe( "POST" ); 33 | 34 | expect( debugReq ).toHaveKey( "body" ); 35 | expect( debugReq.body ).toHaveKey( "body" ); 36 | expect( debugReq.body.body ).toBeArray(); 37 | expect( debugReq.body.body ).toHaveLength( 1 ); 38 | expect( debugReq.body.body[ 1 ] ).toBe( { 39 | "type" : "body", 40 | "value" : binaryBody 41 | } ); 42 | expect( debugReq.body ).toHaveKey( "files" ); 43 | expect( debugReq.body.files ).toBeEmpty(); 44 | 45 | expect( debugReq.body ).toHaveKey( "cookies" ); 46 | expect( debugReq.body.cookies ).toBeArray(); 47 | expect( debugReq.body.cookies ).toHaveLength( 1 ); 48 | expect( debugReq.body.cookies[ 1 ] ).toBe( { "name" : "foo", "value" : "bar" } ); 49 | 50 | expect( debugReq.body ).toHaveKey( "headers" ); 51 | var headers = debugReq.body.headers; 52 | arraySort( headers, function( a, b ) { 53 | return compare( a.name, b.name ); 54 | } ); 55 | expect( headers ).toBeArray(); 56 | expect( headers ).toHaveLength( 4 ); 57 | 58 | expect( headers[ 1 ] ).toBeStruct(); 59 | expect( headers[ 1 ] ).toHaveKey( "name" ); 60 | expect( headers[ 1 ].name ).toBe( "Accept" ); 61 | expect( headers[ 1 ] ).toHaveKey( "value" ); 62 | expect( headers[ 1 ].value ).toBe( "application/json;odata=verbose" ); 63 | 64 | expect( headers[ 2 ] ).toBeStruct(); 65 | expect( headers[ 2 ] ).toHaveKey( "name" ); 66 | expect( headers[ 2 ].name ).toBe( "Authorization" ); 67 | expect( headers[ 2 ] ).toHaveKey( "value" ); 68 | expect( headers[ 2 ].value ).toBe( "Bearer token" ); 69 | 70 | expect( headers[ 3 ] ).toBeStruct(); 71 | expect( headers[ 3 ] ).toHaveKey( "name" ); 72 | expect( headers[ 3 ].name ).toBe( "Content-Type" ); 73 | expect( headers[ 3 ] ).toHaveKey( "value" ); 74 | expect( headers[ 3 ].value ).toBe( "application/octet-stream" ); 75 | 76 | expect( headers[ 4 ] ).toBeStruct(); 77 | expect( headers[ 4 ] ).toHaveKey( "name" ); 78 | expect( headers[ 4 ].name ).toBe( "User-Agent" ); 79 | expect( headers[ 4 ] ).toHaveKey( "value" ); 80 | expect( headers[ 4 ].value ).toBe( "HyperCFML/#req.getHyperVersion()#" ); 81 | } ); 82 | } ); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PRs and Branches 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "main" 7 | - "master" 8 | - "development" 9 | pull_request: 10 | branches: 11 | - main 12 | - master 13 | - development 14 | 15 | jobs: 16 | tests: 17 | runs-on: ubuntu-latest 18 | name: Tests 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "boxlang-cfml@1"] 23 | coldbox: ["^6.0.0", "^7.0.0", "^8.0.0"] 24 | experimental: [ false ] 25 | include: 26 | - cfengine: "lucee@be" 27 | coldbox: "^6.0.0" 28 | experimental: true 29 | - cfengine: "lucee@be" 30 | coldbox: "^7.0.0" 31 | experimental: true 32 | - cfengine: "lucee@be" 33 | coldbox: "^8.0.0" 34 | experimental: true 35 | - cfengine: "adobe@be" 36 | coldbox: "^6.0.0" 37 | experimental: true 38 | - cfengine: "adobe@2025" 39 | coldbox: "^7.0.0" 40 | experimental: false 41 | - cfengine: "adobe@2025" 42 | coldbox: "^8.0.0" 43 | experimental: false 44 | - cfengine: "adobe@be" 45 | coldbox: "^7.0.0" 46 | experimental: true 47 | - cfengine: "adobe@be" 48 | coldbox: "^8.0.0" 49 | experimental: true 50 | - cfengine: "boxlang@be" 51 | coldbox: "^8.0.0" 52 | experimental: true 53 | - cfengine: "boxlang@1" 54 | coldbox: "^8.0.0" 55 | experimental: false 56 | steps: 57 | - name: Checkout Repository 58 | uses: actions/checkout@v3.2.0 59 | 60 | - name: Setup Java JDK 61 | uses: actions/setup-java@v3.9.0 62 | with: 63 | distribution: 'zulu' 64 | java-version: 21 65 | 66 | - name: Setup CommandBox CLI 67 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 68 | with: 69 | install: commandbox-boxlang,commandbox-cfconfig 70 | 71 | - name: Install dependencies 72 | run: | 73 | box install 74 | box install coldbox@${{ matrix.coldbox }} --noSave 75 | 76 | - name: Start server 77 | run: | 78 | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug 79 | curl http://127.0.0.1:8500 80 | 81 | - name: Run TestBox Tests 82 | continue-on-error: ${{ matrix.experimental }} 83 | run: box testbox run 84 | 85 | format: 86 | runs-on: ubuntu-latest 87 | name: Format 88 | steps: 89 | - name: Checkout Repository 90 | uses: actions/checkout@v3.2.0 91 | 92 | - name: Setup Java JDK 93 | uses: actions/setup-java@v3.9.0 94 | with: 95 | distribution: 'zulu' 96 | java-version: 11 97 | 98 | - name: Setup CommandBox CLI 99 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 100 | 101 | - name: Install CFFormat 102 | run: box install commandbox-cfformat 103 | 104 | - name: Run CFFormat 105 | run: box run-script format 106 | 107 | - name: Commit Format Changes 108 | uses: stefanzweifel/git-auto-commit-action@v4 109 | with: 110 | commit_message: Apply cfformat changes -------------------------------------------------------------------------------- /tests/resources/app/tests/specs/integration/MainBDDTest.cfc: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Integration Test as BDD 3 | * 4 | * Extends the integration class: coldbox.system.testing.BaseTestCase 5 | * 6 | * so you can test your ColdBox application headlessly. The 'appMapping' points by default to 7 | * the '/root' mapping created in the test folder Application.cfc. Please note that this 8 | * Application.cfc must mimic the real one in your root, including ORM settings if needed. 9 | * 10 | * The 'execute()' method is used to execute a ColdBox event, with the following arguments 11 | * * event : the name of the event 12 | * * private : if the event is private or not 13 | * * prePostExempt : if the event needs to be exempt of pre post interceptors 14 | * * eventArguments : The struct of args to pass to the event 15 | * * renderResults : Render back the results of the event 16 | *******************************************************************************/ 17 | component extends="coldbox.system.testing.BaseTestCase" appMapping="/root"{ 18 | 19 | /*********************************** LIFE CYCLE Methods ***********************************/ 20 | 21 | function beforeAll(){ 22 | super.beforeAll(); 23 | // do your own stuff here 24 | } 25 | 26 | function afterAll(){ 27 | // do your own stuff here 28 | super.afterAll(); 29 | } 30 | 31 | /*********************************** BDD SUITES ***********************************/ 32 | 33 | function run(){ 34 | 35 | describe( "Main Handler", function(){ 36 | 37 | beforeEach(function( currentSpec ){ 38 | // Setup as a new ColdBox request, VERY IMPORTANT. ELSE EVERYTHING LOOKS LIKE THE SAME REQUEST. 39 | setup(); 40 | }); 41 | 42 | it( "+homepage renders", function(){ 43 | var event = execute( event="main.index", renderResults=true ); 44 | expect( event.getValue( name="welcomemessage", private=true ) ).toBe( "Welcome to ColdBox!" ); 45 | }); 46 | 47 | it( "+doSomething relocates", function(){ 48 | var event = execute( event="main.doSomething" ); 49 | expect( event.getValue( "setnextevent_event", "" ) ).toBe( "main.index" ); 50 | }); 51 | 52 | it( "+app start fires", function(){ 53 | var event = execute( "main.onAppInit" ); 54 | }); 55 | 56 | it( "+can handle exceptions", function(){ 57 | //You need to create an exception bean first and place it on the request context FIRST as a setup. 58 | var exceptionBean = createMock( "coldbox.system.web.context.ExceptionBean" ) 59 | .init( erroStruct=structnew(), extramessage="My unit test exception", extraInfo="Any extra info, simple or complex" ); 60 | 61 | // Attach to request 62 | getRequestContext().setValue( name="exception", value=exceptionBean, private=true ); 63 | 64 | //TEST EVENT EXECUTION 65 | var event = execute( "main.onException" ); 66 | }); 67 | 68 | describe( "Request Events", function(){ 69 | 70 | it( "+fires on start", function(){ 71 | var event = execute( "main.onRequestStart" ); 72 | }); 73 | 74 | it( "+fires on end", function(){ 75 | var event = execute( "main.onRequestEnd" ); 76 | }); 77 | 78 | }); 79 | 80 | describe( "Session Events", function(){ 81 | 82 | it( "+fires on start", function(){ 83 | var event = execute( "main.onSessionStart" ); 84 | }); 85 | 86 | it( "+fires on end", function(){ 87 | //Place a fake session structure here, it mimics what the handler receives 88 | URL.sessionReference = structnew(); 89 | URL.applicationReference = structnew(); 90 | var event = execute( "main.onSessionEnd" ); 91 | }); 92 | 93 | }); 94 | 95 | 96 | }); 97 | 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | jobs: 10 | tests: 11 | name: Tests 12 | if: "!contains(github.event.head_commit.message, '__SEMANTIC RELEASE VERSION UPDATE__')" 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "boxlang-cfml@1"] 18 | coldbox: ["^6.0.0", "^7.0.0", "^8.0.0"] 19 | experimental: [ false ] 20 | include: 21 | - cfengine: "adobe@2025" 22 | coldbox: "^7.0.0" 23 | experimental: false 24 | - cfengine: "adobe@2025" 25 | coldbox: "^8.0.0" 26 | experimental: false 27 | - cfengine: "boxlang@1" 28 | coldbox: "^8.0.0" 29 | experimental: false 30 | steps: 31 | - name: Checkout Repository 32 | uses: actions/checkout@v3.2.0 33 | 34 | - name: Setup Java JDK 35 | uses: actions/setup-java@v3.9.0 36 | with: 37 | distribution: 'zulu' 38 | java-version: 21 39 | 40 | - name: Setup CommandBox CLI 41 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 42 | with: 43 | install: commandbox-boxlang,commandbox-cfconfig 44 | 45 | - name: Install dependencies 46 | run: | 47 | box install 48 | box install coldbox@${{ matrix.coldbox }} --noSave 49 | 50 | - name: Start server 51 | run: | 52 | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug 53 | curl http://127.0.0.1:8500 54 | 55 | - name: Run TestBox Tests 56 | continue-on-error: ${{ matrix.experimental }} 57 | run: box testbox run 58 | 59 | release: 60 | name: Semantic Release 61 | if: "!contains(github.event.head_commit.message, '__SEMANTIC RELEASE VERSION UPDATE__')" 62 | needs: tests 63 | runs-on: ubuntu-latest 64 | env: 65 | GA_COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 66 | steps: 67 | - name: Checkout Repository 68 | uses: actions/checkout@v3.2.0 69 | with: 70 | fetch-depth: 0 71 | 72 | - name: Setup Java JDK 73 | uses: actions/setup-java@v3.9.0 74 | with: 75 | distribution: 'zulu' 76 | java-version: 21 77 | 78 | - name: Setup CommandBox CLI 79 | uses: Ortus-Solutions/setup-commandbox@v2.0.1 80 | 81 | - name: Install and Configure Semantic Release 82 | run: | 83 | box install commandbox-semantic-release@^3.0.0 84 | box config set endpoints.forgebox.APIToken=${{ secrets.FORGEBOX_TOKEN }} 85 | box config set modules.commandbox-semantic-release.targetBranch=main 86 | box config set modules.commandbox-semantic-release.plugins='{ "VerifyConditions": "GitHubActionsConditionsVerifier@commandbox-semantic-release", "FetchLastRelease": "ForgeBoxReleaseFetcher@commandbox-semantic-release", "RetrieveCommits": "JGitCommitsRetriever@commandbox-semantic-release", "ParseCommit": "ConventionalChangelogParser@commandbox-semantic-release", "FilterCommits": "DefaultCommitFilterer@commandbox-semantic-release", "AnalyzeCommits": "DefaultCommitAnalyzer@commandbox-semantic-release", "VerifyRelease": "NullReleaseVerifier@commandbox-semantic-release", "GenerateNotes": "GitHubMarkdownNotesGenerator@commandbox-semantic-release", "UpdateChangelog": "FileAppendChangelogUpdater@commandbox-semantic-release", "CommitArtifacts": "GitHubArtifactsCommitter@commandbox-semantic-release", "PublishRelease": "ForgeBoxReleasePublisher@commandbox-semantic-release", "PublicizeRelease": "GitHubReleasePublicizer@commandbox-semantic-release" }' 87 | 88 | - name: Run Semantic Release 89 | env: 90 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | run: box semantic-release -------------------------------------------------------------------------------- /tests/resources/app/layouts/Main.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to Coldbox! 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 35 | 36 | 37 | 38 | 76 | 77 | 78 |
#renderView()#
79 | 80 | 93 | 94 | 102 | 103 | 104 |
105 | -------------------------------------------------------------------------------- /tests/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | #testbox.init( directory=rootMapping & url.path ).run()# 27 | 28 |

Invalid incoming directory: #rootMapping & url.path#

29 |
30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | TestBox Browser 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 | 69 |
70 | v#testbox.getVersion()# 71 |
72 | 73 |
74 |
75 |
76 |
77 |
78 | 79 |

TestBox Test Browser:

80 |

81 | Below is a listing of the files and folders starting from your root #rootPath#. You can click on individual tests in order to execute them 82 | or click on the Run All button on your left and it will execute a directory runner from the visible folder. 83 |

84 | 85 |
86 | Contents: #executePath# 87 | 88 |

89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ✚ #qResults.name#
98 | 99 | target="_blank"
>#qResults.name#
100 | 101 | target="_blank">#qResults.name#
102 | 103 | #qResults.name#
104 | 105 | 106 |
107 |
108 |
109 |
110 |
111 |
112 | 113 | 114 | 115 |
116 | -------------------------------------------------------------------------------- /tests/resources/app/tests/specs/integration/MainTest.cfc: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 25 | // Call the super setup method to setup the app. 26 | super.setup(); 27 | 28 | // Any preparation work will go here for this test. 29 | 30 | 31 | 32 | 33 | 34 | var event = ""; 35 | 36 | // Place any variables on the form or URL scope to test the handler event 37 | // URL.name = "luis" 38 | event = execute( event="main.index", renderResults=true ); 39 | 40 | //debug(event.getCollection()); 41 | 42 | //Do your asserts below 43 | $assert.isEqual( "Welcome to ColdBox!", event.getValue( "welcomeMessage", "", true ) ); 44 | 45 | 46 | 47 | 48 | 49 | 50 | var event = ""; 51 | 52 | //Place any variables on the form or URL scope to test the handler. 53 | //URL.name = "luis" 54 | event = execute("main.doSomething"); 55 | 56 | // debug(event.getCollection()); 57 | 58 | //Do your asserts below for setnextevent you can test for a setnextevent boolean flag 59 | $assert.isEqual( "main.index", event.getValue( "setnextevent_event", "" ), "Relocation Test" ); 60 | 61 | 62 | 63 | 64 | 65 | var event = ""; 66 | 67 | //Place any variables on the form or URL scope to test the handler. 68 | //URL.name = "luis" 69 | event = execute( "main.onAppInit" ); 70 | 71 | //Do your asserts below 72 | 73 | 74 | 75 | 76 | 77 | 78 | var event = ""; 79 | 80 | //Place any variables on the form or URL scope to test the handler. 81 | //URL.name = "luis" 82 | event = execute( "main.onRequestStart" ); 83 | 84 | //Do your asserts below 85 | 86 | 87 | 88 | 89 | 90 | 91 | var event = ""; 92 | 93 | //Place any variables on the form or URL scope to test the handler. 94 | //URL.name = "luis" 95 | event = execute( "main.onRequestEnd" ); 96 | 97 | //Do your asserts below 98 | 99 | 100 | 101 | 102 | 103 | 104 | var event = ""; 105 | 106 | //Place any variables on the form or URL scope to test the handler. 107 | //URL.name = "luis" 108 | event = execute( "main.onSessionStart" ); 109 | 110 | //Do your asserts below 111 | 112 | 113 | 114 | 115 | 116 | 117 | var event = ""; 118 | var sessionReference = ""; 119 | 120 | //Place a fake session structure here, it mimics what the handler receives 121 | URL.sessionReference = structnew(); 122 | URL.applicationReference = structnew(); 123 | 124 | event = execute( "main.onSessionEnd" ); 125 | 126 | //Do your asserts below 127 | 128 | 129 | 130 | 131 | 132 | 133 | //You need to create an exception bean first and place it on the request context FIRST as a setup. 134 | var exceptionBean = createMock( "coldbox.system.web.context.ExceptionBean" ) 135 | .init( erroStruct=structnew(), extramessage="My unit test exception", extraInfo="Any extra info, simple or complex" ); 136 | 137 | // Attach to request 138 | getRequestContext().setValue( name="exception", value=exceptionBean, private=true ); 139 | 140 | var event = ""; 141 | 142 | //TEST EVENT EXECUTION 143 | event = execute( "main.onException" ); 144 | 145 | //Do your asserts HERE 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /tests/specs/integration/RetrySpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | function beforeAll() { 4 | super.beforeAll(); 5 | addMatchers( "hyper.models.TestBoxMatchers" ); 6 | } 7 | 8 | function run() { 9 | describe( "retry requests", () => { 10 | it( "can retry requests", () => { 11 | var hyper = new hyper.models.HyperBuilder(); 12 | hyper 13 | .fake( { 14 | "https://needs-retry.dev/" : function( createFakeResponse ) { 15 | return [ 16 | createFakeResponse( 500, "Internal Server Error" ), 17 | createFakeResponse( 200, "OK" ) 18 | ]; 19 | } 20 | } ) 21 | .preventStrayRequests(); 22 | 23 | var retryDelays = []; 24 | var onHyperRequestCalls = []; 25 | var onHyperResponseCalls = []; 26 | 27 | var res = hyper 28 | .retry( 3, 100 ) 29 | .withRequestCallback( ( req ) => retryDelays.append( req.getRetries()[ req.getCurrentRequestCount() ] ) ) 30 | .withRequestCallback( ( req ) => onHyperRequestCalls.append( req.getMemento() ) ) 31 | .withResponseCallback( ( res ) => onHyperResponseCalls.append( res.getMemento() ) ) 32 | .get( "https://needs-retry.dev/" ); 33 | 34 | expect( res.getStatusCode() ).toBe( 200 ); 35 | expect( res.getStatusText() ).toBe( "OK" ); 36 | 37 | expect( retryDelays ).toBe( [ 100, 100 ] ); 38 | expect( onHyperRequestCalls ).toHaveLength( 2 ); 39 | expect( onHyperResponseCalls ).toHaveLength( 2 ); 40 | } ); 41 | 42 | it( "can provide a custom array of retry delays", () => { 43 | var hyper = new hyper.models.HyperBuilder(); 44 | hyper 45 | .fake( { 46 | "https://needs-retry.dev/" : function( createFakeResponse ) { 47 | return [ 48 | createFakeResponse( 500, "Internal Server Error" ), 49 | createFakeResponse( 500, "Internal Server Error" ), 50 | createFakeResponse( 200, "OK" ) 51 | ]; 52 | } 53 | } ) 54 | .preventStrayRequests(); 55 | 56 | var retryDelays = []; 57 | var onHyperRequestCalls = []; 58 | var onHyperResponseCalls = []; 59 | 60 | var res = hyper 61 | .retry( [ 100, 200, 300 ] ) 62 | .withRequestCallback( ( req ) => retryDelays.append( req.getRetries()[ req.getCurrentRequestCount() ] ) ) 63 | .withRequestCallback( ( req ) => onHyperRequestCalls.append( req.getMemento() ) ) 64 | .withResponseCallback( ( res ) => onHyperResponseCalls.append( res.getMemento() ) ) 65 | .get( "https://needs-retry.dev/" ); 66 | 67 | expect( res.getStatusCode() ).toBe( 200 ); 68 | expect( res.getStatusText() ).toBe( "OK" ); 69 | 70 | expect( retryDelays ).toBe( [ 100, 200, 300 ] ); 71 | expect( onHyperRequestCalls ).toHaveLength( 3 ); 72 | expect( onHyperResponseCalls ).toHaveLength( 3 ); 73 | } ); 74 | 75 | it( "can provide a predicate function to determine if a request should be retried", () => { 76 | var hyper = new hyper.models.HyperBuilder(); 77 | hyper 78 | .fake( { 79 | "https://needs-retry.dev/" : function( createFakeResponse ) { 80 | return [ 81 | createFakeResponse( 500, "Internal Server Error" ), 82 | createFakeResponse( 429, "Too Many Requests" ), 83 | createFakeResponse( 200, "OK" ) 84 | ]; 85 | } 86 | } ) 87 | .preventStrayRequests(); 88 | 89 | var onHyperRequestCalls = []; 90 | var onHyperResponseCalls = []; 91 | 92 | var res = hyper 93 | .retry( 94 | 3, 95 | 100, 96 | function( res, req ) { 97 | return res.isServerError(); 98 | } 99 | ) 100 | .withRequestCallback( ( req ) => onHyperRequestCalls.append( req.getMemento() ) ) 101 | .withResponseCallback( ( res ) => onHyperResponseCalls.append( res.getMemento() ) ) 102 | .get( "https://needs-retry.dev/" ); 103 | 104 | expect( res.getStatusCode() ).toBe( 429 ); 105 | expect( res.getStatusText() ).toBe( "Too Many Requests" ); 106 | 107 | expect( onHyperRequestCalls ).toHaveLength( 2 ); 108 | expect( onHyperResponseCalls ).toHaveLength( 2 ); 109 | } ); 110 | 111 | it( "can modify the next request from the predicate function", () => { 112 | var hyper = new hyper.models.HyperBuilder(); 113 | hyper 114 | .fake( { 115 | "https://needs-retry.dev/failure" : function( createFakeResponse ) { 116 | return createFakeResponse( 500, "Internal Server Error" ); 117 | }, 118 | "https://needs-retry.dev/success" : function( createFakeResponse ) { 119 | return createFakeResponse( 200, "OK" ); 120 | } 121 | } ) 122 | .preventStrayRequests(); 123 | 124 | var onHyperRequestCalls = []; 125 | var onHyperResponseCalls = []; 126 | 127 | var res = hyper 128 | .setBaseUrl( "https://needs-retry.dev" ) 129 | .retry( 130 | 3, 131 | 100, 132 | function( res, req ) { 133 | req.setUrl( "/success" ); 134 | return res.isError(); 135 | } 136 | ) 137 | .withRequestCallback( ( req ) => onHyperRequestCalls.append( req.getMemento() ) ) 138 | .withResponseCallback( ( res ) => onHyperResponseCalls.append( res.getMemento() ) ) 139 | .get( "/failure" ); 140 | 141 | expect( res.getStatusCode() ).toBe( 200 ); 142 | expect( res.getStatusText() ).toBe( "OK" ); 143 | 144 | expect( onHyperRequestCalls ).toHaveLength( 2 ); 145 | expect( onHyperResponseCalls ).toHaveLength( 2 ); 146 | } ); 147 | } ); 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /tests/specs/integration/GetSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | function run() { 4 | describe( "GET requests", function() { 5 | beforeEach( function() { 6 | variables.hyper = getInstance( "HyperBuilder@Hyper" ); 7 | } ); 8 | 9 | afterEach( function() { 10 | if ( structKeyExists( variables, "hyper" ) ) { 11 | structDelete( variables, "hyper" ); 12 | } 13 | } ); 14 | 15 | it( "can make a GET request", function() { 16 | var res = hyper.get( "https://jsonplaceholder.typicode.com/posts/1" ); 17 | expect( res ).toBeInstanceOf( "HyperResponse", "A HyperResponse object should have been returned." ); 18 | var data = res.json(); 19 | expect( data ).toBeStruct( "Expected to deserialize JSON data from the response." ); 20 | expect( data ).toBe( 21 | deserializeJSON( 22 | "{ 23 | ""userId"": 1, 24 | ""id"" : 1, 25 | ""title"" : ""sunt aut facere repellat provident occaecati excepturi optio reprehenderit"", 26 | ""body"" : ""quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"" 27 | }" 28 | ) 29 | ); 30 | } ); 31 | 32 | it( "serializes query string params in the short hand", function() { 33 | var res = hyper.get( "https://jsonplaceholder.typicode.com/posts", { "userId" : 1 } ); 34 | expect( res.getRequest().getFullUrl( withQueryString = true ) ).toBeWithCase( 35 | "https://jsonplaceholder.typicode.com/posts?userId=1" 36 | ); 37 | } ); 38 | 39 | it( "serializes query string params in the long hand", function() { 40 | var res = hyper 41 | .setBaseUrl( "https://jsonplaceholder.typicode.com" ) 42 | .setUrl( "/posts" ) 43 | .withQueryParams( { "userId" : 1 } ) 44 | .get(); 45 | expect( res.getRequest().getFullUrl( withQueryString = true ) ).toBeWithCase( 46 | "https://jsonplaceholder.typicode.com/posts?userId=1" 47 | ); 48 | } ); 49 | 50 | it( "deserializes query string parameters in the url (to reserialize later)", function() { 51 | var req = hyper 52 | .setBaseUrl( "https://jsonplaceholder.typicode.com" ) 53 | .setUrl( "/posts?param=with+spaces" ); 54 | var res = req.get(); 55 | expect( res.getRequest().getFullUrl( withQueryString = true ) ).toBeWithCase( 56 | "https://jsonplaceholder.typicode.com/posts?param=with+spaces" 57 | ); 58 | } ); 59 | 60 | it( "can handle params with no value in the url", function() { 61 | var res = hyper 62 | .setBaseUrl( "https://jsonplaceholder.typicode.com" ) 63 | .setUrl( "/posts?flag" ) 64 | .get(); 65 | expect( res.getRequest().getFullUrl( withQueryString = true ) ).toBeWithCase( 66 | "https://jsonplaceholder.typicode.com/posts?flag" 67 | ); 68 | } ); 69 | 70 | it( "combines both query params and the query string in the url", function() { 71 | var res = hyper 72 | .setBaseUrl( "https://jsonplaceholder.typicode.com" ) 73 | .setUrl( "/posts?userId=1&fwreinit=true" ) 74 | .withQueryParams( { "force" : "true" } ) 75 | .get(); 76 | expect( res.getRequest().getFullUrl( withQueryString = true ) ).toBeWithCase( 77 | "https://jsonplaceholder.typicode.com/posts?force=true&fwreinit=true&userId=1" 78 | ); 79 | } ); 80 | 81 | it( "can add additional query params with the same name", function() { 82 | var res = hyper 83 | .setBaseUrl( "https://jsonplaceholder.typicode.com" ) 84 | .setUrl( "/posts" ) 85 | .appendQueryParam( "foo", "bar" ) 86 | .appendQueryParam( "foo", "baz" ) 87 | .get(); 88 | expect( res.getRequest().getFullUrl( withQueryString = true ) ).toBeWithCase( 89 | "https://jsonplaceholder.typicode.com/posts?foo=bar&foo=baz" 90 | ); 91 | } ); 92 | 93 | it( "can append multiple query params at once", function() { 94 | var res = hyper 95 | .setBaseUrl( "https://jsonplaceholder.typicode.com" ) 96 | .setUrl( "/posts" ) 97 | .appendQueryParams( { "foo" : "bar" } ) 98 | .appendQueryParams( { "foo" : "baz", "one" : "two" } ) 99 | .get(); 100 | expect( res.getRequest().getFullUrl( withQueryString = true ) ).toBeWithCase( 101 | "https://jsonplaceholder.typicode.com/posts?foo=bar&foo=baz&one=two" 102 | ); 103 | } ); 104 | 105 | it( "has access to the original HyperRequest in the HyperResponse", function() { 106 | var res = hyper.get( "https://jsonplaceholder.typicode.com/posts/1" ); 107 | expect( res ).toBeInstanceOf( "HyperResponse", "A HyperResponse object should have been returned." ); 108 | var req = res.getRequest(); 109 | expect( req ).toBeInstanceOf( "HyperRequest" ); 110 | expect( req.getUrl() ).toBeWithCase( "https://jsonplaceholder.typicode.com/posts/1" ); 111 | } ); 112 | 113 | it( "records the execution time of a request", function() { 114 | var startTick = getTickCount(); 115 | var res = hyper.get( "https://jsonplaceholder.typicode.com/posts/1" ); 116 | var elapsedTime = getTickCount() - startTick; 117 | expect( res.getExecutionTime() ).toBeNumeric(); 118 | expect( res.getExecutionTime() ).toBeGTE( 0, "Execution time should be positive." ); 119 | expect( res.getExecutionTime() ).toBeLTE( 120 | elapsedTime, 121 | "Execution time should be less than the total test execution time." 122 | ); 123 | } ); 124 | } ); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /tests/resources/app/tests/test.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Tests ran at ${start.TODAY} 50 | 51 | 52 | 53 | 54 | 64 | 67 | 68 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 102 | 105 | 106 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /tests/resources/app/tests/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | #testbox.init( directory=rootMapping & url.path ).run()# 28 | 29 |

Invalid incoming directory: #rootMapping & url.path#

30 |
31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | TestBox Global Runner 52 | 53 | 69 | 141 | 142 | 143 | 144 | 145 | 146 |
147 |
148 | 149 | 150 |
151 |
v#testbox.getVersion()#
152 | 153 | 154 |
155 | 156 |
157 |

TestBox Test Browser:

158 |

159 | Below is a listing of the files and folders starting from your root #rootPath#. You can click on individual tests in order to execute them 160 | or click on the Run All button on your left and it will execute a directory runner from the visible folder. 161 |

162 | 163 |
Contents: #executePath# 164 | 165 |

166 |
167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | +#qResults.name#
175 | 176 | target="_blank"
>#qResults.name#
177 | 178 | target="_blank">#qResults.name#
179 | 180 | #qResults.name#
181 | 182 | 183 |
184 |
185 | 186 |
187 | 188 |
189 |
190 | 191 | 192 |
193 | 194 | 195 | 196 |
-------------------------------------------------------------------------------- /tests/resources/app/views/main/index.cfm: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | logo 6 |

7 | 8 | Read ColdBox Manual 9 | 10 |

11 |
12 | 13 |
14 |

#prc.welcomeMessage#

15 |

16 | You are now running #getSetting("codename",1)# #getSetting("version",1)# (#getsetting("suffix",1)#). 17 | Welcome to the next generation of ColdFusion (CFML) applications. You can now start building your application with ease, we already did the hard work 18 | for you. 19 |

20 |
21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 | 33 |

34 | You can click on the following event handlers to execute their default action 35 | index() 36 |

37 | 42 |
43 | 44 |
45 | 50 |

Below are your application's loaded modules, click on them to visit them.

51 | 56 | 57 |
There are no modules in your application
58 |
59 |
60 | 61 |
62 | 67 |

68 | You can find your entire test harness in the following location: #getSetting("ApplicationPath")#tests 69 |

70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 92 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 106 | 107 | 108 | 109 | 112 | 113 | 114 | 115 |
File/FolderDescription
78 | specs 79 | Where all your bdd, module, unit and integration tests go
84 | results 85 | Where automated test results go
90 | resources 91 | 93 | Test resources like fixtures, itegrations, etc. 94 |
98 | Application.cfc 99 | A unique Application.cfc for your testing harness, please spice up as needed.
104 | test.xml 105 | A script for executing all application tests via TestBox ANT
110 | runner.cfm 111 | A TestBox runner so you can execute your tests.
116 |
117 | 118 |
119 | 122 |

ColdBox can use some very important URL actions to interact with your application. You can try them out below:

123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 135 | 136 | 139 | 140 | 141 |
URL ActionDescriptionExecute
132 | ?fwreinit=1
133 | ?fwreinit={ReinitPassword} 134 |
Reinitialize the Application 137 | Execute 138 |
142 |
143 | 144 |
145 | 148 |

149 | You can now start editing your application and building great ColdBox enabled apps. Important files & locations: 150 |

151 |
    152 |
  1. 153 | /config/ColdBox.cfc: Your application configuration file 154 |
  2. 155 |
  3. 156 | /config/Routes.cfm: Your URL Mappings 157 |
  4. 158 |
  5. 159 | /config/WireBox.cfc: Your WireBox Binder 160 |
  6. 161 |
  7. 162 | /handlers: Your application event handlers 163 |
  8. 164 |
  9. 165 | /interceptors: System interceptors 166 |
  10. 167 |
  11. 168 | /includes: Assets, Helpers, i18n, templates and more. 169 |
  12. 170 |
  13. 171 | /layouts:Your application skin layouts 172 |
  14. 173 |
  15. 174 | /lib: Where Jar files can be integrated 175 |
  16. 176 |
  17. 177 | /models: Your model layer 178 |
  18. 179 |
  19. 180 | /modules: Your application modules 181 |
  20. 182 |
  21. 183 | /tests: Your BDD testing harness (Just DO IT!!) 184 |
  22. 185 |
  23. 186 | /views: Your application views 187 |
  24. 188 |
189 |
190 |
191 | 192 | 193 |
194 |
195 | 239 |
240 |
241 |
242 |
-------------------------------------------------------------------------------- /models/HyperBuilder.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates new requests with optional defaults. 3 | */ 4 | component singleton { 5 | 6 | property name="interceptorService" inject="box:interceptorService"; 7 | property name="wirebox" inject="wirebox"; 8 | property name="asyncManager"; // use wirebox to inject this until CommandBox supports `box:asyncManager` 9 | 10 | /** 11 | * Create a new HyperBuilder. 12 | * Any arguments passed are set on the default request. 13 | * 14 | * @returns The new HyperBuilder instance. 15 | */ 16 | public HyperBuilder function init( defaults = new Hyper.models.HyperRequest() ) { 17 | this.defaults = arguments.defaults; 18 | for ( var key in arguments ) { 19 | if ( key == "defaults" ) { 20 | continue; 21 | } 22 | 23 | invoke( 24 | this.defaults, 25 | "set#key#", 26 | [ arguments[ key ] ] 27 | ); 28 | } 29 | return this; 30 | } 31 | 32 | public void function onDIComplete() { 33 | if ( structKeyExists( variables, "interceptorService" ) ) { 34 | this.defaults.setInterceptorService( variables.interceptorService ); 35 | } 36 | 37 | if ( structKeyExists( variables, "wirebox" ) ) { 38 | variables.asyncManager = variables.wirebox.getAsyncManager(); 39 | } 40 | 41 | if ( structKeyExists( variables, "asyncManager" ) ) { 42 | this.defaults.setAsyncManager( variables.asyncManager ); 43 | } 44 | } 45 | 46 | /** 47 | * Registers a new custom Hyper client with the given request. 48 | * 49 | * @alias The WireBox mapping for the custom client. 50 | * @defaults The request to use as the defaults for the custom client. 51 | * 52 | * @throws HyperNonColdBoxContext 53 | * @returns The HyperRequest instance being used as the defaults. 54 | */ 55 | public HyperRequest function registerAs( required string alias, required HyperRequest defaults ) { 56 | if ( !structKeyExists( variables, "wirebox" ) ) { 57 | throw( 58 | type = "HyperNonColdBoxContext", 59 | message = "No wirebox instance available to register custom client. Are you sure you are running in a ColdBox context?" 60 | ); 61 | } 62 | 63 | variables.wirebox 64 | .getBinder() 65 | .forceMap( alias ) 66 | .to( "hyper.models.HyperBuilder" ) 67 | .asSingleton() 68 | .initWith( defaults = arguments.defaults ); 69 | 70 | return arguments.defaults; 71 | } 72 | 73 | /** 74 | * Create a new request from the default request. 75 | * 76 | * @returns A new HyperRequest instance from the default request. 77 | */ 78 | public HyperRequest function new() { 79 | var req = this.defaults.clone(); 80 | req.setBuilder( this ); 81 | if ( !isNull( variables.fakeConfiguration ) ) { 82 | req.setFakeConfiguration( variables.fakeConfiguration ); 83 | } 84 | if ( !isNull( variables.prevent ) ) { 85 | req.setPreventStrayRequests( variables.prevent ); 86 | } 87 | return req; 88 | } 89 | 90 | /** 91 | * Sets this HyperBuilder to fake HTTP requests. Used primarily for testing. 92 | * Accepts an optional configuration struct of patterns to response generator functions. 93 | * Usually called in a `beforeAll` block. 94 | * 95 | * @configuration An optional configuration struct of patterns to response generator functions. 96 | * 97 | * @returns The HyperBuilder instance. 98 | */ 99 | public HyperBuilder function fake( struct configuration = {} ) { 100 | variables.fakeConfiguration = arguments.configuration; 101 | return this; 102 | } 103 | 104 | /** 105 | * Sets the HyperBuilder to prevent stray requests. 106 | * Must be called after setting the builder to `fake()`. 107 | * 108 | * @prevent Whether to prevent stray requests. Defaults: `true`. 109 | * 110 | * @throws HyperFakeNotEnabled 111 | * 112 | * @returns The HyperBuilder instance. 113 | */ 114 | public HyperBuilder function preventStrayRequests( boolean prevent = true ) { 115 | if ( !structKeyExists( variables, "fakeConfiguration" ) ) { 116 | throw( type = "HyperFakeNotEnabled", message = "Call `fake()` before calling `preventStrayRequests()`." ); 117 | } 118 | variables.prevent = arguments.prevent; 119 | return this; 120 | } 121 | 122 | /** 123 | * Records a request and fake response for later assertions. 124 | * 125 | * @req The HyperRequest being made. 126 | * @res The FakeHyperResponse being returned. 127 | * 128 | * @returns The FakeHyperResponse being recorded. 129 | */ 130 | public FakeHyperResponse function record( required HyperRequest req, required FakeHyperResponse res ) { 131 | param variables.requests = []; 132 | variables.requests.append( { 133 | req : arguments.req, 134 | res : arguments.res 135 | } ); 136 | return arguments.res; 137 | } 138 | 139 | /** 140 | * Registers a sequence of responses for a given pattern. 141 | * Called when returning an array from the response generator function. 142 | * 143 | * @pattern The pattern the sequence is registered against. 144 | * @sequence The array of FakeHyperResponse instances to return in order. 145 | * 146 | * @returns The HyperBuilder instance. 147 | */ 148 | public HyperBuilder function registerSequence( required string pattern, required array sequence ) { 149 | param variables.sequences = {}; 150 | variables.sequences[ arguments.pattern ] = arguments.sequence; 151 | return this; 152 | } 153 | 154 | /** 155 | * Returns whether a sequence has been registered for a given pattern. 156 | * 157 | * @pattern The pattern to check for a registered sequence. 158 | * 159 | * @returns boolean 160 | */ 161 | public boolean function hasSequenceForPattern( required string pattern ) { 162 | param variables.sequences = {}; 163 | return structKeyExists( variables.sequences, arguments.pattern ); 164 | } 165 | 166 | /** 167 | * Returns the next response in the sequence for a given pattern. 168 | * 169 | * @pattern The pattern to return the next response for. 170 | * 171 | * @throws HyperFakeSequenceMissing 172 | * @throws HyperFakeSequenceExhausted 173 | * 174 | * @returns The next FakeHyperResponse in the sequence. 175 | */ 176 | public FakeHyperResponse function popResponseForSequence( required string pattern ) { 177 | param variables.sequences = {}; 178 | if ( !structKeyExists( variables.sequences, arguments.pattern ) ) { 179 | throw( 180 | type = "HyperFakeSequenceMissing", 181 | message = "No sequence registered for pattern #arguments.pattern#", 182 | detail = "Registered patterns are: [#variables.sequences.keyArray().toList( ", " )#]" 183 | ); 184 | } 185 | var sequence = variables.sequences[ arguments.pattern ]; 186 | param variables.sequenceIndexes = {}; 187 | if ( !variables.sequenceIndexes.keyExists( arguments.pattern ) ) { 188 | variables.sequenceIndexes[ arguments.pattern ] = 1; 189 | } 190 | var sequenceIndex = variables.sequenceIndexes[ arguments.pattern ]; 191 | if ( sequenceIndex > sequence.len() ) { 192 | throw( 193 | type = "HyperFakeSequenceExhausted", 194 | message = "Sequence for pattern #arguments.pattern# is out of responses." 195 | ); 196 | } 197 | var nextRes = sequence[ sequenceIndex ]; 198 | variables.sequenceIndexes[ arguments.pattern ] = sequenceIndex + 1; 199 | return nextRes; 200 | } 201 | 202 | /** 203 | * Returns the number of fake requests that have been made. 204 | * 205 | * @returns numeric 206 | */ 207 | public numeric function getFakeRequestCount() { 208 | param variables.requests = []; 209 | return variables.requests.len(); 210 | } 211 | 212 | /** 213 | * Returns whether a request has been made that matches the callback. 214 | * Each request that has been made is passed to the predicate. 215 | * If the predicate returns true, the request is considered a match and true is returned. 216 | * If no request passed the predicate, false is returned. 217 | * 218 | * @predicate A callback function that returns true if the request matches the criteria and returns false otherwise. 219 | * 220 | * @returns boolean 221 | */ 222 | public boolean function wasRequestSent( required function predicate ) { 223 | for ( var record in variables.requests ) { 224 | var matched = arguments.predicate( record.req ); 225 | if ( matched ) { 226 | return true; 227 | } 228 | } 229 | return false; 230 | } 231 | 232 | /** 233 | * Clears the fake configuration from this HyperBuilder. 234 | * Usually called in an `afterAll` block. 235 | * 236 | * @returns The HyperBuilder instance. 237 | */ 238 | public HyperBuilder function clearFakes() { 239 | structDelete( variables, "sequences" ); 240 | structDelete( variables, "sequenceIndexes" ); 241 | structDelete( variables, "originalSequences" ); 242 | structDelete( variables, "requests" ); 243 | structDelete( variables, "prevent" ); 244 | structDelete( variables, "fakeConfiguration" ); 245 | return this; 246 | } 247 | 248 | /** 249 | * Resets the requests made and fake responses received for the HyperBuilder. 250 | * The configuration remains intact and sequences are reset. 251 | * Usually called in an `afterEach` block. 252 | * 253 | * @returns The HyperBuilder instance. 254 | */ 255 | public HyperBuilder function resetFakes() { 256 | structDelete( variables, "requests" ); 257 | structDelete( variables, "sequenceIndexes" ); 258 | return this; 259 | } 260 | 261 | /** 262 | * Forward on other calls to a new request instance. 263 | */ 264 | function onMissingMethod( missingMethodName, missingMethodArguments ) { 265 | return invoke( 266 | variables.new(), 267 | missingMethodName, 268 | missingMethodArguments 269 | ); 270 | } 271 | 272 | } 273 | -------------------------------------------------------------------------------- /tests/specs/integration/FakeSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" { 2 | 3 | function beforeAll() { 4 | super.beforeAll(); 5 | addMatchers( "hyper.models.TestBoxMatchers" ); 6 | } 7 | 8 | function run() { 9 | describe( "fake requests", () => { 10 | it( "can fake all requests", () => { 11 | var hyper = new hyper.models.HyperBuilder(); 12 | hyper.fake(); 13 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 14 | expect( res.getStatusCode() ).toBe( 200 ); 15 | expect( res.getStatusText() ).toBe( "OK" ); 16 | } ); 17 | 18 | it( "can return a fake response for a pattern", () => { 19 | var hyper = new hyper.models.HyperBuilder(); 20 | hyper.fake( { 21 | "https://does-not-exist.also-does-not-exist" : function( createFakeResponse ) { 22 | return createFakeResponse( 404, "Not Found" ); 23 | } 24 | } ); 25 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 26 | expect( res.getStatusCode() ).toBe( 404 ); 27 | expect( res.getStatusText() ).toBe( "Not Found" ); 28 | } ); 29 | 30 | it( "receives the request when generating fake responses", () => { 31 | var hyper = new hyper.models.HyperBuilder(); 32 | hyper.fake( { 33 | "https://does-not-exist.also-does-not-exist" : function( createFakeResponse, req ) { 34 | expect( req ).toBeInstanceOf( "hyper.models.HyperRequest" ); 35 | return createFakeResponse( 404, "Not Found" ); 36 | } 37 | } ); 38 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 39 | } ); 40 | 41 | it( "can return a sequence of responses", () => { 42 | var hyper = new hyper.models.HyperBuilder(); 43 | hyper.fake( { 44 | "https://does-not-exist.also-does-not-exist" : function( createFakeResponse ) { 45 | return [ 46 | createFakeResponse( 200, "OK" ), 47 | createFakeResponse( 404, "Not Found" ) 48 | ]; 49 | } 50 | } ); 51 | 52 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 53 | expect( res.getStatusCode() ).toBe( 200 ); 54 | expect( res.getStatusText() ).toBe( "OK" ); 55 | 56 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 57 | expect( res.getStatusCode() ).toBe( 404 ); 58 | expect( res.getStatusText() ).toBe( "Not Found" ); 59 | 60 | expect( function() { 61 | hyper.get( "https://does-not-exist.also-does-not-exist" ); 62 | } ).toThrow( type = "HyperFakeSequenceExhausted" ); 63 | } ); 64 | 65 | it( "can prevent stray requests from executing and throw an exception", () => { 66 | var hyper = new hyper.models.HyperBuilder(); 67 | hyper.fake( { 68 | "https://does-not-exist.also-does-not-exist" : function( createFakeResponse, req ) { 69 | expect( req ).toBeInstanceOf( "hyper.models.HyperRequest" ); 70 | return createFakeResponse( 404, "Not Found" ); 71 | } 72 | } ); 73 | var res = hyper.get( "https://google.com" ); 74 | expect( res.getStatusCode() ).toBe( 200 ); 75 | expect( res.getStatusText() ).toBe( "OK" ); 76 | 77 | hyper.preventStrayRequests(); 78 | 79 | expect( function() { 80 | hyper.get( "https://google.com" ); 81 | } ).toThrow( type = "HyperFakeStrayRequest" ); 82 | } ); 83 | 84 | it( "can check if a request was sent or not", () => { 85 | var hyper = new hyper.models.HyperBuilder(); 86 | hyper.fake(); 87 | 88 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 89 | 90 | expect( 91 | hyper.wasRequestSent( function( req ) { 92 | return req.getUrl() == "https://does-not-exist.also-does-not-exist"; 93 | } ) 94 | ).toBeTrue( "Expected the request to have been sent" ); 95 | 96 | expect( 97 | hyper.wasRequestSent( function( req ) { 98 | return req.getUrl() == "https://google.com"; 99 | } ) 100 | ).toBeFalse( "Expected the request not to have been sent" ); 101 | } ); 102 | 103 | it( "can assert a request was sent", () => { 104 | var hyper = new hyper.models.HyperBuilder(); 105 | hyper.fake(); 106 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 107 | 108 | expect( function() { 109 | expect( hyper ).toHaveSentRequest( function( req ) { 110 | return req.getUrl() == "https://does-not-exist.also-does-not-exist"; 111 | }, "Custom Message" ); 112 | } ).notToThrow(); 113 | 114 | expect( function() { 115 | expect( hyper ).toHaveSentRequest( function( req ) { 116 | return req.getUrl() == "https://google.com"; 117 | } ); 118 | } ).toThrow( 119 | type = "TestBox.AssertionFailed", 120 | regex = "Expected to find a request that matched the callback parameters but did not." 121 | ); 122 | 123 | expect( function() { 124 | expect( hyper ).toHaveSentRequest( function( req ) { 125 | return req.getUrl() == "https://google.com"; 126 | }, "Custom Message" ); 127 | } ).toThrow( type = "TestBox.AssertionFailed", regex = "Custom Message" ); 128 | } ); 129 | 130 | it( "can assert a request was not sent", () => { 131 | var hyper = new hyper.models.HyperBuilder(); 132 | hyper.fake(); 133 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 134 | 135 | expect( function() { 136 | expect( hyper ).notToHaveSentRequest( function( req ) { 137 | return req.getUrl() == "https://does-not-exist.also-does-not-exist"; 138 | } ); 139 | } ).toThrow( 140 | type = "TestBox.AssertionFailed", 141 | regex = "Expected to not find a request that matched the callback parameters but did." 142 | ); 143 | 144 | expect( function() { 145 | expect( hyper ).notToHaveSentRequest( function( req ) { 146 | return req.getUrl() == "https://google.com"; 147 | } ); 148 | } ).notToThrow(); 149 | } ); 150 | 151 | it( "can show the sent request count from when the client was faked", () => { 152 | var hyper = new hyper.models.HyperBuilder(); 153 | hyper.fake(); 154 | 155 | expect( hyper.getFakeRequestCount() ).toBe( 0 ); 156 | expect( function() { 157 | expect( hyper ).toHaveSentCount( 0 ); 158 | } ).notToThrow( type = "TestBox.AssertionFailed" ); 159 | expect( function() { 160 | expect( hyper ).toHaveSentCount( 1 ); 161 | } ).toThrow( type = "TestBox.AssertionFailed" ); 162 | 163 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 164 | 165 | expect( hyper.getFakeRequestCount() ).toBe( 1 ); 166 | expect( function() { 167 | expect( hyper ).toHaveSentCount( 1 ); 168 | } ).notToThrow( type = "TestBox.AssertionFailed" ); 169 | expect( function() { 170 | expect( hyper ).toHaveSentCount( 0 ); 171 | } ).toThrow( type = "TestBox.AssertionFailed" ); 172 | 173 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 174 | 175 | expect( hyper.getFakeRequestCount() ).toBe( 2 ); 176 | expect( function() { 177 | expect( hyper ).toHaveSentCount( 2 ); 178 | } ).notToThrow( type = "TestBox.AssertionFailed" ); 179 | 180 | var res = hyper.get( "https://google.com" ); 181 | 182 | expect( hyper.getFakeRequestCount() ).toBe( 3 ); 183 | expect( function() { 184 | expect( hyper ).toHaveSentCount( 3 ); 185 | } ).notToThrow( type = "TestBox.AssertionFailed" ); 186 | } ); 187 | 188 | it( "can assert nothing was sent", () => { 189 | var hyper = new hyper.models.HyperBuilder(); 190 | hyper.fake(); 191 | 192 | expect( function() { 193 | expect( hyper ).toHaveSentNothing(); 194 | } ).notToThrow( type = "TestBox.AssertionFailed" ); 195 | 196 | hyper.get( "https://google.com" ); 197 | 198 | expect( function() { 199 | expect( hyper ).toHaveSentNothing(); 200 | } ).toThrow( type = "TestBox.AssertionFailed" ); 201 | } ); 202 | 203 | it( "can clear all fake requests to make normal requests again", () => { 204 | var hyper = new hyper.models.HyperBuilder(); 205 | hyper.fake(); 206 | 207 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 208 | 209 | expect( res.getStatusCode() ).toBe( 200 ); 210 | expect( res.getStatusText() ).toBe( "OK" ); 211 | 212 | hyper.clearFakes(); 213 | 214 | var res = hyper.get( "https://does-not-exist.also-does-not-exist" ); 215 | 216 | expect( res.getStatusCode() ).toBe( 502 ); 217 | expect( res.getStatusText() ).toBe( "Bad Gateway" ); 218 | } ); 219 | 220 | it( "can reset fake requests and restart sequences", () => { 221 | var hyper = new hyper.models.HyperBuilder(); 222 | hyper.fake( { 223 | "https://does-not-exist.also-does-not-exist" : function( createFakeResponse ) { 224 | return [ 225 | createFakeResponse( 200, "OK" ), 226 | createFakeResponse( 404, "Not Found" ) 227 | ]; 228 | } 229 | } ); 230 | 231 | var resA = hyper.get( "https://does-not-exist.also-does-not-exist" ); 232 | expect( resA.getStatusCode() ).toBe( 200 ); 233 | expect( resA.getStatusText() ).toBe( "OK" ); 234 | 235 | var resB = hyper.get( "https://does-not-exist.also-does-not-exist" ); 236 | expect( resB.getStatusCode() ).toBe( 404 ); 237 | expect( resB.getStatusText() ).toBe( "Not Found" ); 238 | 239 | expect( hyper ).toHaveSentCount( 2 ); 240 | 241 | hyper.resetFakes(); 242 | 243 | expect( hyper ).toHaveSentCount( 0 ); 244 | 245 | var resC = hyper.get( "https://does-not-exist.also-does-not-exist" ); 246 | expect( resC.getStatusCode() ).toBe( 200 ); 247 | expect( resC.getStatusText() ).toBe( "OK" ); 248 | 249 | expect( hyper ).toHaveSentCount( 1 ); 250 | } ); 251 | } ); 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /tests/specs/unit/HyperRequestSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "HyperRequest", function() { 5 | beforeEach( function() { 6 | variables.req = new Hyper.models.HyperRequest(); 7 | } ); 8 | 9 | afterEach( function() { 10 | if ( variables.keyExists( "req" ) ) { 11 | variables.delete( "req" ); 12 | } 13 | } ); 14 | 15 | it( "can serialize to a memento", function() { 16 | expect( variables.req.getMemento() ).toBe( { 17 | "requestID" : variables.req.getRequestID(), 18 | "baseUrl" : variables.req.getBaseUrl(), 19 | "url" : variables.req.getUrl(), 20 | "fullUrl" : variables.req.getFullUrl(), 21 | "method" : variables.req.getMethod(), 22 | "queryParams" : variables.req.getQueryParams(), 23 | "headers" : variables.req.getHeaders(), 24 | "cookies" : variables.req.getCookies(), 25 | "files" : variables.req.getFiles(), 26 | "bodyFormat" : variables.req.getBodyFormat(), 27 | "body" : variables.req.getBody(), 28 | "referrerId" : "", 29 | "throwOnError" : variables.req.getThrowOnError(), 30 | "timeout" : variables.req.getTimeout(), 31 | "maximumRedirects" : variables.req.getMaximumRedirects(), 32 | "authType" : variables.req.getAuthType(), 33 | "username" : variables.req.getUsername(), 34 | "password" : variables.req.getPassword(), 35 | "clientCert" : "", 36 | "clientCertPassword" : "", 37 | "domain" : variables.req.getDomain(), 38 | "workstation" : variables.req.getWorkstation(), 39 | "resolveUrls" : variables.req.getResolveUrls(), 40 | "encodeUrl" : variables.req.getEncodeUrl(), 41 | "retries" : variables.req.getRetries(), 42 | "currentRequestCount" : variables.req.getCurrentRequestCount() 43 | } ); 44 | } ); 45 | 46 | it( "can exclude keys from the memento", () => { 47 | var memento = variables.req.getMemento( excludes = [ "cookies" ] ); 48 | expect( memento ).notToHaveKey( "cookies" ); 49 | } ); 50 | 51 | it( "can set multiple values at once from a struct", function() { 52 | expect( req.getUrl() ).toBe( "" ); 53 | expect( req.getMethod() ).toBe( "GET" ); 54 | req.setProperties( { 55 | "url" : "https://jsonplaceholder.typicode.com/posts/1", 56 | "method" : "PATCH" 57 | } ); 58 | expect( req.getUrl() ).toBe( "https://jsonplaceholder.typicode.com/posts/1" ); 59 | expect( req.getMethod() ).toBe( "PATCH" ); 60 | } ); 61 | 62 | it( "can set multiple headers at once", function() { 63 | expect( req.getHeader( "Accept" ) ).toBe( "" ); 64 | expect( req.getHeader( "X-Requested-With" ) ).toBe( "" ); 65 | req.withHeaders( { 66 | "Accept" : "application/xml", 67 | "X-Requested-With" : "XMLHTTPRequest" 68 | } ); 69 | expect( req.getHeader( "Accept" ) ).toBe( "application/xml" ); 70 | expect( req.getHeader( "X-Requested-With" ) ).toBe( "XMLHTTPRequest" ); 71 | } ); 72 | 73 | it( "preserves case in header names", function() { 74 | expect( req.getHeader( "Accept" ) ).toBe( "" ); 75 | req.withHeaders( { "accept" : "application/xml" } ); 76 | req.setHeader( "accept", "application/xml" ); 77 | expect( req.getHeader( "Accept" ) ).toBe( "" ); 78 | expect( req.getHeader( "accept" ) ).toBe( "application/xml" ); 79 | req.withHeaders( { "Accept" : "application/xml" } ); 80 | expect( req.getHeader( "Accept" ) ).toBe( "application/xml" ); 81 | } ); 82 | 83 | it( "can set multiple cookies at once", function() { 84 | expect( req.getCookie( "foo" ) ).toBe( "" ); 85 | expect( req.getCookie( "baz" ) ).toBe( "" ); 86 | req.withCookies( { "foo" : "bar", "baz" : "qux" } ); 87 | expect( req.getCookie( "foo" ) ).toBe( "bar" ); 88 | expect( req.getCookie( "baz" ) ).toBe( "qux" ); 89 | } ); 90 | 91 | it( "throws an exception if the url is empty when trying to make a request", function() { 92 | expect( function() { 93 | req.get(); 94 | } ).toThrow( "NoUrlException" ); 95 | } ); 96 | 97 | it( "can clear out all values", function() { 98 | req.setUrl( "https://jsonplaceholder.typicode.com/posts/1" ) 99 | .setMethod( "PATCH" ) 100 | .withHeaders( { "Accept" : "application/xml" } ); 101 | 102 | expect( req.getUrl() ).toBe( "https://jsonplaceholder.typicode.com/posts/1" ); 103 | expect( req.getMethod() ).toBe( "PATCH" ); 104 | expect( req.getHeader( "Accept" ) ).toBe( "application/xml" ); 105 | 106 | req.clear(); 107 | 108 | expect( req.getUrl() ).toBe( "" ); 109 | expect( req.getMethod() ).toBe( "GET" ); 110 | expect( req.getHeader( "Accept" ) ).toBe( "" ); 111 | } ); 112 | 113 | it( "can chain conditional request methods with when", function() { 114 | req.when( 115 | true, 116 | function( r ) { 117 | r.withHeaders( { "Accept" : "application/xml" } ); 118 | }, 119 | function( r ) { 120 | r.withHeaders( { "Accept" : "application/json" } ); 121 | } 122 | ); 123 | 124 | expect( req.getHeader( "Accept" ) ).toBe( "application/xml" ); 125 | 126 | req.clear(); 127 | 128 | req.when( 129 | false, 130 | function( r ) { 131 | r.withHeaders( { "Accept" : "application/xml" } ); 132 | }, 133 | function( r ) { 134 | r.withHeaders( { "Accept" : "application/json" } ); 135 | } 136 | ); 137 | 138 | expect( req.getHeader( "Accept" ) ).toBe( "application/json" ); 139 | 140 | req.clear(); 141 | 142 | req.when( false, function( r ) { 143 | r.withHeaders( { "Accept" : "application/xml" } ); 144 | } ); 145 | 146 | expect( req.getHeader( "Accept" ) ).toBe( "" ); 147 | } ); 148 | 149 | it( "can set the resolvesUrls functionality of cfhttp", function() { 150 | expect( req.getResolveUrls() ).toBeFalse(); 151 | req.resolveUrls(); 152 | expect( req.getResolveUrls() ).toBeTrue(); 153 | } ); 154 | 155 | it( "can set the client certificate path", function() { 156 | expect( req.getClientCert() ).toBeNull(); 157 | req.withCertificateAuth( "/some/absolute/path" ); 158 | expect( req.getClientCert() ).toBe( "/some/absolute/path" ); 159 | } ); 160 | 161 | it( "can include a password when setting the certificate authentication", function() { 162 | expect( req.getClientCert() ).toBeNull(); 163 | expect( req.getClientCertPassword() ).toBeNull(); 164 | req.withCertificateAuth( "/some/absolute/path", "mypassword" ); 165 | expect( req.getClientCert() ).toBe( "/some/absolute/path" ); 166 | expect( req.getClientCertPassword() ).toBe( "mypassword" ); 167 | } ); 168 | 169 | it( "can define onRequest callback hooks", function() { 170 | var method = "not set yet"; 171 | var headers = {}; 172 | req.setRequestCallbacks( [ 173 | function( req ) { 174 | method = req.getMethod(); 175 | } 176 | ] ); 177 | req.withRequestCallback( function( req ) { 178 | headers = req.getHeaders(); 179 | } ); 180 | 181 | req.withHeaders( { "Accept" : "application/xml" } ) 182 | .asJSON() 183 | .patch( "https://jsonplaceholder.typicode.com/posts/1" ); 184 | expect( method ).toBe( "PATCH" ); 185 | expect( headers ).toBe( { 186 | "Accept" : "application/xml", 187 | "Content-Type" : "application/json", 188 | "User-Agent" : "HyperCFML/#req.getHyperVersion()#" 189 | } ); 190 | } ); 191 | 192 | it( "can define onResponse callback hooks", function() { 193 | var responseId = "not set yet"; 194 | var statusCode = 0; 195 | req.setResponseCallbacks( [ 196 | function( res ) { 197 | responseId = res.getResponseId(); 198 | } 199 | ] ); 200 | req.withResponseCallback( function( res ) { 201 | statusCode = res.getStatusCode(); 202 | } ); 203 | 204 | var res = req.post( 205 | "https://jsonplaceholder.typicode.com/posts", 206 | { 207 | title : "New title", 208 | body : "New body", 209 | userId : 1 210 | } 211 | ); 212 | expect( responseId ).notToBe( "not set yet" ); 213 | expect( responseId ).toBe( res.getResponseId() ); 214 | expect( statusCode ).toBe( 201 ); 215 | } ); 216 | 217 | it( "can forward on headers if they exist", function() { 218 | expect( req.hasHeader( "X-Requested-With" ) ).toBeFalse(); 219 | expect( req.hasHeader( "X-Forwarded-For" ) ).toBeFalse(); 220 | req.forwardHeaders( 221 | [ 222 | "X-Forwarded-For", 223 | "X-Requested-With" 224 | ], 225 | { "X-Forwarded-For" : "1.1.1.1" } 226 | ); 227 | expect( req.hasHeader( "X-Requested-With" ) ).toBeFalse(); 228 | expect( req.hasHeader( "X-Forwarded-For" ) ).toBeTrue(); 229 | expect( req.getHeader( "X-Forwarded-For" ) ).toBe( "1.1.1.1" ); 230 | } ); 231 | 232 | it( "can handle a JSON body format with a body as a string", function() { 233 | req.setBodyFormat( "json" ); 234 | req.setBody( '{"query":{},"size":0,"from":0}' ); 235 | expect( req.prepareBody() ).toBe( '{"query":{},"size":0,"from":0}' ); 236 | } ); 237 | 238 | it( "can handle a JSON body format with a body as an struct", function() { 239 | req.setBodyFormat( "json" ); 240 | req.setBody( { "query" : {}, "size" : 0, "from" : 0 } ); 241 | expect( deserializeJSON( req.prepareBody() ) ).toBe( { "query" : {}, "size" : 0, "from" : 0 } ); 242 | } ); 243 | 244 | it( "defaults to no Content-Type", function() { 245 | expect( req.getHeader( "Content-Type" ) ).toBeEmpty(); 246 | } ); 247 | 248 | it( "defaults to the Hyper User-Agent", () => { 249 | expect( req.getHeader( "User-Agent" ) ).toBe( "HyperCFML/#req.getHyperVersion()#" ); 250 | } ); 251 | } ); 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /models/HyperResponse.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * A convenient wrapper for HTTP responses. 3 | */ 4 | component accessors="true" { 5 | 6 | /** 7 | * Unique response ID representing this response. 8 | */ 9 | property name="responseID"; 10 | 11 | /** 12 | * The status code for the response. 13 | */ 14 | property 15 | name ="statusCode" 16 | default="200" 17 | setter ="false"; 18 | 19 | /** 20 | * The status text for the response. 21 | */ 22 | property 23 | name ="statusText" 24 | default="OK" 25 | setter ="false"; 26 | 27 | /** 28 | * The data for the response. 29 | */ 30 | property name="data" default="" setter="false"; 31 | 32 | /** 33 | * The HyperRequest instance associated with this response. 34 | */ 35 | property name="request" setter="false"; 36 | 37 | /** 38 | * The charset value for the response. 39 | */ 40 | property 41 | name ="charset" 42 | default="UTF-8" 43 | setter ="false"; 44 | 45 | /** 46 | * The charset value for the response. 47 | */ 48 | property 49 | name ="headers" 50 | type ="struct" 51 | setter="false"; 52 | 53 | /** 54 | * The timestamp for when this response was received. 55 | */ 56 | property name="timestamp" setter="false"; 57 | 58 | /** 59 | * The execution time of the request, in milliseconds. 60 | */ 61 | property name="executionTime" setter="false"; 62 | 63 | /** 64 | * Create a new HyperResponse. 65 | * 66 | * @originalRequest The HyperRequest associated with this response. 67 | * @executionTime The execution time of the request, in milliseconds. 68 | * @charset The response charset. Default: UTF-8 69 | * @statusCode The response status code. Default: 200. 70 | * @statusText The response status text. Default: OK. 71 | * @headers The response headers. Default: {}. 72 | * @data The response data. Default: "". 73 | * @timestamp The timestamp for when this response was received. Default: `now()`. 74 | * @responseID Unique response ID representing this response. Default: `createUUID()`. 75 | * 76 | * @returns A new HyperResponse instance. 77 | */ 78 | public HyperResponse function init( 79 | required HyperRequest originalRequest, 80 | required numeric executionTime, 81 | string charset = "UTF-8", 82 | numeric statusCode = 200, 83 | string statusText = "OK", 84 | struct headers = {}, 85 | any data = "", 86 | timestamp = now(), 87 | any responseID = createUUID() 88 | ) { 89 | variables.responseID = arguments.responseID; 90 | variables.request = arguments.originalRequest; 91 | variables.charset = arguments.charset; 92 | variables.statusCode = arguments.statusCode; 93 | variables.statusText = arguments.statusText; 94 | variables.headers = arguments.headers; 95 | variables.data = arguments.data; 96 | variables.timestamp = arguments.timestamp; 97 | variables.executionTime = arguments.executionTime; 98 | return this; 99 | } 100 | 101 | /** 102 | * Returns the status code and status text as a single string. 103 | * 104 | * @returns The status code and status text 105 | */ 106 | function getStatus() { 107 | return "#getStatusCode()# #getStatusText()#"; 108 | } 109 | 110 | /** 111 | * Returns the id of the request to which this response is related. 112 | * 113 | * @returns The request id 114 | */ 115 | function getRequestID() { 116 | return getRequest().getRequestID(); 117 | } 118 | 119 | /** 120 | * Returns the data of the request as deserialized JSON. 121 | * 122 | * @returns A deserialized version of the data. 123 | */ 124 | function json() { 125 | if ( !isJSON( getData() ) ) { 126 | throw( 127 | type = "DeserializeJsonException", 128 | message = "The response is not json.", 129 | detail = getData() 130 | ); 131 | } 132 | return deserializeJSON( getData() ); 133 | } 134 | 135 | /** 136 | * Returns true if the request status code is considered successful (2xx status code). 137 | * 138 | * @returns boolean 139 | */ 140 | function isSuccess() { 141 | return left( getStatusCode(), 1 ) == "2"; 142 | } 143 | 144 | /** 145 | * Returns true if the request status code is 200 OK. 146 | * 147 | * @returns boolean 148 | */ 149 | function isOK() { 150 | return getStatusCode() == "200"; 151 | } 152 | 153 | /** 154 | * Returns true if the request status code is 201 Created. 155 | * 156 | * @returns boolean 157 | */ 158 | function isCreated() { 159 | return getStatusCode() == "201"; 160 | } 161 | 162 | /** 163 | * Returns true if the request status code is considered a redirect (3xx status code). 164 | * 165 | * @returns boolean 166 | */ 167 | function isRedirect() { 168 | return left( getStatusCode(), 1 ) == "3"; 169 | } 170 | 171 | /** 172 | * Returns true if the request status code is considered either a 173 | * client error (4xx status code) or a server error (5xx status code). 174 | * 175 | * @returns boolean 176 | */ 177 | function isError() { 178 | return left( getStatusCode(), 1 ) == "4" || 179 | left( getStatusCode(), 1 ) == "5"; 180 | } 181 | 182 | /** 183 | * Returns true if the request status code is considered a client error (4xx status code). 184 | * 185 | * @returns boolean 186 | */ 187 | function isClientError() { 188 | return left( getStatusCode(), 1 ) == "4"; 189 | } 190 | 191 | /** 192 | * Returns true if the request status code is 401 Unauthorized. 193 | * 194 | * @returns boolean 195 | */ 196 | function isUnauthorized() { 197 | return getStatusCode() == "401"; 198 | } 199 | 200 | /** 201 | * Returns true if the request status code is 403 Forbidden. 202 | * 203 | * @returns boolean 204 | */ 205 | function isForbidden() { 206 | return getStatusCode() == "403"; 207 | } 208 | 209 | /** 210 | * Returns true if the request status code is 404 Not Found. 211 | * 212 | * @returns boolean 213 | */ 214 | function isNotFound() { 215 | return getStatusCode() == "404"; 216 | } 217 | 218 | /** 219 | * Returns true if the request status code is considered a server error (5xx status code). 220 | * 221 | * @returns boolean 222 | */ 223 | function isServerError() { 224 | return left( getStatusCode(), 1 ) == "5"; 225 | } 226 | 227 | /** 228 | * Checks if a header exists in the response. 229 | * 230 | * @name The name of the header to check. 231 | * 232 | * @returns boolean 233 | */ 234 | public boolean function hasHeader( required string name ) { 235 | return variables.headers.keyExists( lCase( arguments.name ) ); 236 | } 237 | 238 | /** 239 | * Gets the value of a header from the response. 240 | * 241 | * @name The name of the header to retrieve. 242 | * @defaultValue The value to return if the header does not exist. 243 | * 244 | * @returns any 245 | */ 246 | public any function getHeader( required string name, any defaultValue = "" ) { 247 | return hasHeader( arguments.name ) ? variables.headers[ lCase( arguments.name ) ] : arguments.defaultValue; 248 | } 249 | 250 | /** 251 | * Caches the result of parsing the `Set-Cookie` header and returns it. 252 | * 253 | * @returns struct 254 | */ 255 | public struct function getCookies() { 256 | param variables.cookies = parseCookies(); 257 | return variables.cookies; 258 | } 259 | 260 | /** 261 | * Parses and saves the cookies to the cookie scope. 262 | * 263 | * @returns The HyperResponse instance. 264 | */ 265 | public HyperResponse function persistCookies() { 266 | var cookies = getCookies().each( function( name, cookieStruct ) { 267 | cookieStruct[ "name" ] = name; 268 | cookieStruct[ "preservecase" ] = true; 269 | if ( !cookieStruct.keyExists( "domain" ) ) { 270 | cookieStruct[ "domain" ] = CGI.HTTP_HOST; 271 | } 272 | cfcookie( attributeCollection = cookieStruct ); 273 | } ); 274 | return this; 275 | } 276 | 277 | /** 278 | * Parses the `Set-Cookie` header and returns a struct of cookies for the response. 279 | * 280 | * @returns struct 281 | */ 282 | private struct function parseCookies() { 283 | return arrayWrap( getHeader( "Set-Cookie", [] ) ).reduce( function( acc, cookieString ) { 284 | var parts = listToArray( cookieString, ";" ); 285 | 286 | // the first item is always the name/value pair 287 | var nameValuePair = parts[ 1 ]; 288 | 289 | // grab the name from the name/value pair and set it in our return struct 290 | var name = listFirst( nameValuePair, "=" ); 291 | acc[ name ] = {}; 292 | 293 | // parse out the value, if one exists 294 | acc[ name ][ "value" ] = ""; 295 | if ( listLen( nameValuePair, "=" ) > 1 ) { 296 | acc[ name ][ "value" ] = listRest( nameValuePair, "=" ); 297 | } 298 | 299 | // grab the rest of the parts and parse them out 300 | var rest = arrayLen( parts ) > 1 ? arraySlice( parts, 2 ) : []; 301 | for ( var segment in rest ) { 302 | var segmentParts = listToArray( segment, "=" ); 303 | var segmentName = segmentParts[ 1 ]; 304 | var segmentValue = arrayLen( segmentParts ) > 1 ? segmentParts[ 2 ] : true; 305 | 306 | // CFML doesn't support passing Max-Age so we'll 307 | // convert to days instead and pass as the Expires key 308 | if ( segmentName == "max-age" ) { 309 | segmentName = "expires"; 310 | segmentValue = segmentValue / 60 / 60 / 24; 311 | } 312 | 313 | acc[ name ][ segmentName ] = segmentValue; 314 | } 315 | 316 | return acc; 317 | }, {} ); 318 | } 319 | 320 | /** 321 | * Gets a serializable representation of the response 322 | * 323 | * @returns struct 324 | */ 325 | public struct function getMemento( array excludes = [] ) { 326 | return structFilter( 327 | { 328 | "responseID" : getResponseID(), 329 | "requestID" : getRequestID(), 330 | "statusCode" : getStatusCode(), 331 | "statusText" : getStatusText(), 332 | "status" : getStatus(), 333 | "data" : getData(), 334 | "charset" : getCharset(), 335 | "headers" : getHeaders(), 336 | "timestamp" : getTimestamp(), 337 | "executionTime" : getExecutionTime(), 338 | "cookies" : getCookies() 339 | }, 340 | function( key ) { 341 | return !arrayContainsNoCase( excludes, key ); 342 | } 343 | ); 344 | } 345 | 346 | /** 347 | * Ensures the passed in value is an array 348 | * 349 | * @value A single value or an array. 350 | * 351 | * @returns An array 352 | */ 353 | private array function arrayWrap( required any value ) { 354 | if ( isArray( arguments.value ) ) { 355 | return arguments.value; 356 | } 357 | return [ arguments.value ]; 358 | } 359 | 360 | } 361 | -------------------------------------------------------------------------------- /tests/specs/unit/HyperResponseSpec.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | function run() { 4 | describe( "HyperResponse", function() { 5 | it( "can serialize to a memento", function() { 6 | var res = new Hyper.models.HyperResponse( 7 | originalRequest = createStub( extends = "models.HyperRequest" ).$( "getRequestID", createUUID() ), 8 | statusCode = 200, 9 | executionTime = 100, 10 | headers = { "Content-Type" : "text/html; charset=utf-8" } 11 | ); 12 | 13 | expect( res.getMemento() ).toBe( { 14 | "responseID" : res.getResponseID(), 15 | "requestID" : res.getRequestID(), 16 | "statusCode" : res.getStatusCode(), 17 | "statusText" : res.getStatusText(), 18 | "status" : res.getStatus(), 19 | "data" : res.getData(), 20 | "charset" : res.getCharset(), 21 | "headers" : res.getHeaders(), 22 | "timestamp" : res.getTimestamp(), 23 | "executionTime" : res.getExecutionTime(), 24 | "cookies" : res.getCookies() 25 | } ); 26 | } ); 27 | 28 | it( "can exclude keys from the memento", function() { 29 | var res = new Hyper.models.HyperResponse( 30 | originalRequest = createStub( extends = "models.HyperRequest" ).$( "getRequestID", createUUID() ), 31 | statusCode = 200, 32 | executionTime = 100, 33 | headers = { "Content-Type" : "text/html; charset=utf-8" } 34 | ); 35 | 36 | var memento = res.getMemento( excludes = [ "cookies" ] ); 37 | expect( memento ).notToHaveKey( "cookies" ); 38 | } ); 39 | 40 | it( "throws an exception when requesting json data but the data is not json", function() { 41 | var res = new Hyper.models.HyperResponse( 42 | originalRequest = createStub( extends = "models.HyperRequest" ), 43 | charset = "UTF-8", 44 | statusCode = 200, 45 | headers = { "status_code" : "200" }, 46 | data = "definitely not json", 47 | executionTime = 100 48 | ); 49 | expect( function() { 50 | var json = res.json(); 51 | } ).toThrow( "DeserializeJsonException" ); 52 | } ); 53 | 54 | describe( "status code detection", function() { 55 | it( "can tell if a request is a success", function() { 56 | var res = new Hyper.models.HyperResponse( 57 | originalRequest = createStub( extends = "models.HyperRequest" ), 58 | statusCode = 200, 59 | statusText = "OK", 60 | executionTime = 100 61 | ); 62 | expect( res.isSuccess() ).toBeTrue(); 63 | expect( res.isRedirect() ).toBeFalse(); 64 | expect( res.isError() ).toBeFalse(); 65 | expect( res.isClientError() ).toBeFalse(); 66 | expect( res.isServerError() ).toBeFalse(); 67 | } ); 68 | 69 | it( "can tell if a request is an OK response", function() { 70 | var resA = new Hyper.models.HyperResponse( 71 | originalRequest = createStub( extends = "models.HyperRequest" ), 72 | statusCode = 200, 73 | statusText = "OK", 74 | executionTime = 100 75 | ); 76 | expect( resA.isOK() ).toBeTrue(); 77 | 78 | var resB = new Hyper.models.HyperResponse( 79 | originalRequest = createStub( extends = "models.HyperRequest" ), 80 | statusCode = 204, 81 | executionTime = 100 82 | ); 83 | expect( resB.isOK() ).toBeFalse(); 84 | } ); 85 | 86 | it( "can tell if a request is a redirect", function() { 87 | var res = new Hyper.models.HyperResponse( 88 | originalRequest = createStub( extends = "models.HyperRequest" ), 89 | statusCode = 302, 90 | statusText = "Found", 91 | executionTime = 100 92 | ); 93 | expect( res.isSuccess() ).toBeFalse(); 94 | expect( res.isRedirect() ).toBeTrue(); 95 | expect( res.isError() ).toBeFalse(); 96 | expect( res.isClientError() ).toBeFalse(); 97 | expect( res.isServerError() ).toBeFalse(); 98 | } ); 99 | 100 | it( "can tell if a request is a client error", function() { 101 | var res = new Hyper.models.HyperResponse( 102 | originalRequest = createStub( extends = "models.HyperRequest" ), 103 | statusCode = 422, 104 | statusText = "Unprocessable Entity", 105 | executionTime = 100 106 | ); 107 | expect( res.isSuccess() ).toBeFalse(); 108 | expect( res.isRedirect() ).toBeFalse(); 109 | expect( res.isError() ).toBeTrue(); 110 | expect( res.isClientError() ).toBeTrue(); 111 | expect( res.isServerError() ).toBeFalse(); 112 | } ); 113 | 114 | it( "can tell if a request is an unauthorized error", function() { 115 | var resA = new Hyper.models.HyperResponse( 116 | originalRequest = createStub( extends = "models.HyperRequest" ), 117 | statusCode = 401, 118 | statusText = "Unauthorized", 119 | executionTime = 100 120 | ); 121 | expect( resA.isUnauthorized() ).toBeTrue(); 122 | 123 | var resB = new Hyper.models.HyperResponse( 124 | originalRequest = createStub( extends = "models.HyperRequest" ), 125 | statusCode = 400, 126 | executionTime = 100 127 | ); 128 | expect( resB.isUnauthorized() ).toBeFalse(); 129 | } ); 130 | 131 | it( "can tell if a request is a forbidden error", function() { 132 | var resA = new Hyper.models.HyperResponse( 133 | originalRequest = createStub( extends = "models.HyperRequest" ), 134 | statusCode = 403, 135 | statusText = "Forbidden", 136 | executionTime = 100 137 | ); 138 | expect( resA.isForbidden() ).toBeTrue(); 139 | 140 | var resB = new Hyper.models.HyperResponse( 141 | originalRequest = createStub( extends = "models.HyperRequest" ), 142 | statusCode = 400, 143 | executionTime = 100 144 | ); 145 | expect( resB.isForbidden() ).toBeFalse(); 146 | } ); 147 | 148 | it( "can tell if a request is a not found error", function() { 149 | var resA = new Hyper.models.HyperResponse( 150 | originalRequest = createStub( extends = "models.HyperRequest" ), 151 | statusCode = 404, 152 | statusText = "Not Found", 153 | executionTime = 100 154 | ); 155 | expect( resA.isNotFound() ).toBeTrue(); 156 | 157 | var resB = new Hyper.models.HyperResponse( 158 | originalRequest = createStub( extends = "models.HyperRequest" ), 159 | statusCode = 401, 160 | executionTime = 100 161 | ); 162 | expect( resB.isNotFound() ).toBeFalse(); 163 | } ); 164 | 165 | it( "can tell if a request is a server error", function() { 166 | var res = new Hyper.models.HyperResponse( 167 | originalRequest = createStub( extends = "models.HyperRequest" ), 168 | statusCode = 500, 169 | statusText = "Internal Server Error", 170 | executionTime = 100 171 | ); 172 | expect( res.isSuccess() ).toBeFalse(); 173 | expect( res.isRedirect() ).toBeFalse(); 174 | expect( res.isError() ).toBeTrue(); 175 | expect( res.isClientError() ).toBeFalse(); 176 | expect( res.isServerError() ).toBeTrue(); 177 | } ); 178 | 179 | it( "can accept a customer status text", function() { 180 | var res = new Hyper.models.HyperResponse( 181 | originalRequest = createStub( extends = "models.HyperRequest" ), 182 | statusCode = 500, 183 | statusText = "Boom", 184 | executionTime = 100 185 | ); 186 | expect( res.getStatusCode() ).toBe( 500 ); 187 | expect( res.getStatusText() ).toBe( "Boom" ); 188 | expect( res.getStatus() ).toBe( "500 Boom" ); 189 | } ); 190 | } ); 191 | 192 | it( "can check if a response header exists", function() { 193 | var res = new Hyper.models.HyperResponse( 194 | originalRequest = createStub( extends = "models.HyperRequest" ), 195 | statusCode = 200, 196 | executionTime = 100, 197 | headers = { "Content-Type" : "text/html; charset=utf-8" } 198 | ); 199 | expect( res.hasHeader( "Etag" ) ).toBeFalse(); 200 | expect( res.hasHeader( "Content-Type" ) ).toBeTrue(); 201 | } ); 202 | 203 | it( "can get the value of a response header", function() { 204 | var res = new Hyper.models.HyperResponse( 205 | originalRequest = createStub( extends = "models.HyperRequest" ), 206 | statusCode = 200, 207 | executionTime = 100, 208 | headers = { "Content-Type" : "text/html; charset=utf-8" } 209 | ); 210 | expect( res.getHeader( "Content-Type" ) ).toBe( "text/html; charset=utf-8" ); 211 | } ); 212 | 213 | it( "can provide a default value for a response header", function() { 214 | var res = new Hyper.models.HyperResponse( 215 | originalRequest = createStub( extends = "models.HyperRequest" ), 216 | statusCode = 200, 217 | executionTime = 100, 218 | headers = {} 219 | ); 220 | expect( res.getHeader( "Content-Type", "text/html; charset=utf-8" ) ).toBe( "text/html; charset=utf-8" ); 221 | } ); 222 | 223 | it( "can get the returned cookies as a struct", function() { 224 | var res = new Hyper.models.HyperResponse( 225 | originalRequest = createStub( extends = "models.HyperRequest" ), 226 | statusCode = 200, 227 | executionTime = 100, 228 | headers = { 229 | "Set-Cookie" : [ 230 | "foo=bar;path=/;secure;samesite=none;httponly", 231 | "baz=qux;path=/;expires=Fri, 31 Dec 2038 23:59:59 GMT", 232 | "one=two;max-age=2592000;domain=example.com" 233 | ] 234 | } 235 | ); 236 | expect( res.getCookies() ).toBe( { 237 | "foo" : { 238 | "value" : "bar", 239 | "path" : "/", 240 | "secure" : true, 241 | "samesite" : "none", 242 | "httponly" : true 243 | }, 244 | "baz" : { 245 | "value" : "qux", 246 | "path" : "/", 247 | "expires" : "Fri, 31 Dec 2038 23:59:59 GMT" 248 | }, 249 | "one" : { 250 | "value" : "two", 251 | "domain" : "example.com", 252 | "expires" : 2592000 / 60 / 60 / 24 // seconds to days 253 | } 254 | } ); 255 | } ); 256 | 257 | it( "can save cookies to the cookie scope", function() { 258 | var res = new Hyper.models.HyperResponse( 259 | originalRequest = createStub( extends = "models.HyperRequest" ), 260 | statusCode = 200, 261 | executionTime = 100, 262 | headers = { 263 | "Set-Cookie" : [ 264 | "foo=bar;path=/;secure;samesite=none;httponly", 265 | "baz=qux;path=/;expires=Fri, 31 Dec 2038 23:59:59 GMT", 266 | "one=two;max-age=2592000;domain=example.com" 267 | ] 268 | } 269 | ); 270 | for ( var key in cookie ) { 271 | cfcookie( 272 | name = key, 273 | value = "", 274 | expires = "now" 275 | ); 276 | } 277 | expect( cookie ).notToHaveKey( "foo" ); 278 | expect( cookie ).notToHaveKey( "baz" ); 279 | expect( cookie ).notToHaveKey( "one" ); 280 | 281 | res.persistCookies(); 282 | 283 | expect( cookie ).toHaveKey( "foo" ); 284 | expect( cookie.foo ).toBe( "bar" ); 285 | expect( cookie ).toHaveKey( "baz" ); 286 | expect( cookie.baz ).toBe( "qux" ); 287 | expect( cookie ).toHaveKey( "one" ); 288 | expect( cookie.one ).toBe( "two" ); 289 | } ); 290 | } ); 291 | } 292 | 293 | } 294 | -------------------------------------------------------------------------------- /models/CfhttpHttpClient.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Uses CFHTTP to execute HyperRequests. 3 | */ 4 | component implements="HyperHttpClientInterface" { 5 | 6 | /** 7 | * Execute the HyperRequest and map it to a HyperResponse. 8 | * 9 | * @req The HyperRequest to execute. 10 | * 11 | * @returns A HyperResponse of the executed request. 12 | */ 13 | public HyperResponse function send( required HyperRequest req ) { 14 | var startTick = getTickCount(); 15 | var cfhttpResponse = makeCFHTTPRequest( req ); 16 | var res = new Hyper.models.HyperResponse( 17 | originalRequest = req, 18 | executionTime = getTickCount() - startTick, 19 | charset = cfhttpResponse.charset ?: "UTF-8", 20 | statusCode = normalizeStatusCode( cfhttpResponse ), 21 | statusText = normalizeStatusText( cfhttpResponse ), 22 | headers = normalizeHeaders( cfhttpResponse ), 23 | data = cfhttpResponse.filecontent 24 | ); 25 | 26 | if ( req.getThrowOnError() && res.isError() ) { 27 | throw( 28 | type = "HyperRequestError", 29 | message = "Received a [#res.getStatus()#] response when requesting [#req.getFullUrl()#]", 30 | detail = res.getData(), 31 | extendedinfo = serializeJSON( { 32 | "request" : req.getMemento(), 33 | "response" : res.getMemento() 34 | } ) 35 | ); 36 | } 37 | 38 | return res; 39 | } 40 | 41 | /** 42 | * Return a struct of information showing how the client will execute the HyperRequest. 43 | * This will be used by a developer to debug any differences between the generated 44 | * request values and the expected request values. 45 | * 46 | * @req The HyperRequest to debug. 47 | * 48 | * @returns A struct of information detailing how the client would execute the HyperRequest. 49 | */ 50 | public struct function debug( required HyperRequest req ) { 51 | var attrCollection = { 52 | "timeout" : req.getTimeout(), 53 | "url" : req.getFullUrl(), 54 | "method" : req.getMethod(), 55 | "redirect" : false, 56 | "resolveURL" : req.getResolveUrls() 57 | }; 58 | 59 | if ( !req.getEncodeUrl() ) { 60 | attrCollection[ "encodeurl" ] = false; 61 | } 62 | 63 | if ( len( req.getUsername() ) ) { 64 | attrCollection[ "username" ] = req.getUsername(); 65 | } 66 | 67 | if ( len( req.getPassword() ) ) { 68 | attrCollection[ "password" ] = req.getPassword(); 69 | } 70 | 71 | if ( len( req.getDomain() ) ) { 72 | attrCollection[ "domain" ] = req.getDomain(); 73 | } 74 | 75 | if ( len( req.getWorkStation() ) ) { 76 | attrCollection[ "workstation" ] = req.getWorkStation(); 77 | } 78 | 79 | // this is only necessary for NTLM authType, BASIC is the default 80 | if ( len( req.getAuthType() ) && len( req.getUsername() ) ) { 81 | attrCollection[ "authType" ] = req.getAuthType(); 82 | } 83 | 84 | if ( !isNull( req.getClientCert() ) ) { 85 | attrCollection[ "clientCert" ] = req.getClientCert(); 86 | } 87 | 88 | if ( !isNull( req.getClientCertPassword() ) ) { 89 | attrCollection[ "clientCertPassword" ] = req.getClientCertPassword(); 90 | } 91 | 92 | var cfhttpHeaders = []; 93 | var headers = req.getHeaders(); 94 | for ( var name in headers ) { 95 | // we want to skip adding a Content-Type header when there are files 96 | // so that the CFML engines can add the correct boundary to the Content-Type 97 | if ( name == "Content-Type" && !req.getFiles().isEmpty() ) { 98 | continue; 99 | } 100 | 101 | cfhttpHeaders.append( { 102 | "name" : name, 103 | "value" : headers[ name ] 104 | } ); 105 | } 106 | 107 | var cfhttpParams = []; 108 | var queryParams = req.getQueryParams(); 109 | for ( var param in queryParams ) { 110 | cfhttpParams.append( { 111 | "name" : param.name, 112 | "value" : param.value 113 | } ); 114 | } 115 | 116 | var cfhttpFiles = []; 117 | for ( var file in req.getFiles() ) { 118 | var fileAttrCollection = { name : file.name, file : file.path }; 119 | if ( file.keyExists( "mimeType" ) && !isNull( file.mimeType ) ) { 120 | fileAttrCollection[ "mimeType" ] = file.mimeType; 121 | } 122 | cfhttpFiles.append( fileAttrCollection ); 123 | } 124 | 125 | var cfhttpCookies = req 126 | .getCookies() 127 | .reduce( function( acc, name, value ) { 128 | acc.append( { "name" : name, "value" : value } ); 129 | return acc; 130 | }, [] ); 131 | 132 | var cfhttpBody = []; 133 | if ( req.hasBody() ) { 134 | switch ( req.getBodyFormat() ) { 135 | case "json": 136 | // this is here for backwards compatibility 137 | if ( !headers.keyExists( "Content-Type" ) ) { 138 | cfhttpHeaders.append( { 139 | "name" : "Content-Type", 140 | "value" : "application/json" 141 | } ); 142 | } 143 | 144 | cfhttpBody.append( { 145 | "type" : "body", 146 | "value" : req.prepareBody() 147 | } ); 148 | break; 149 | case "formFields": 150 | var body = req.getBody(); 151 | for ( var fieldName in body ) { 152 | for ( var value in arrayWrap( body[ fieldName ] ) ) { 153 | cfhttpBody.append( { 154 | "type" : "formfield", 155 | "name" : fieldName, 156 | "value" : value 157 | } ); 158 | } 159 | } 160 | break; 161 | case "xml": 162 | cfhttpBody.append( { 163 | "type" : "xml", 164 | "value" : req.prepareBody() 165 | } ); 166 | break; 167 | default: 168 | cfhttpBody.append( { 169 | "type" : "body", 170 | "value" : req.prepareBody() 171 | } ); 172 | } 173 | } 174 | 175 | return { 176 | "attributes" : attrCollection, 177 | "body" : { 178 | "headers" : cfhttpHeaders, 179 | "params" : cfhttpParams, 180 | "files" : cfhttpFiles, 181 | "body" : cfhttpBody, 182 | "cookies" : cfhttpCookies 183 | } 184 | }; 185 | } 186 | 187 | /** 188 | * Makes an HTTP request using CFHTTP. 189 | * 190 | * @req The request to execute 191 | * 192 | * @returns The CFHTTP response struct. 193 | */ 194 | private struct function makeCFHTTPRequest( required HyperRequest req ) { 195 | local.res = ""; 196 | var attrCollection = { 197 | "result" : "local.res", 198 | "timeout" : req.getTimeout(), 199 | "url" : req.getFullUrl(), 200 | "method" : req.getMethod(), 201 | "redirect" : false, 202 | "resolveURL" : req.getResolveUrls() 203 | }; 204 | 205 | if ( !req.getEncodeUrl() ) { 206 | attrCollection[ "encodeurl" ] = false; 207 | } 208 | 209 | if ( len( req.getUsername() ) ) { 210 | attrCollection[ "username" ] = req.getUsername(); 211 | } 212 | 213 | if ( len( req.getPassword() ) ) { 214 | attrCollection[ "password" ] = req.getPassword(); 215 | } 216 | 217 | if ( len( req.getDomain() ) ) { 218 | attrCollection[ "domain" ] = req.getDomain(); 219 | } 220 | 221 | if ( len( req.getWorkStation() ) ) { 222 | attrCollection[ "workstation" ] = req.getWorkStation(); 223 | } 224 | 225 | // this is only necessary for NTLM authType, BASIC is the default 226 | if ( len( req.getAuthType() ) && len( req.getUsername() ) ) { 227 | attrCollection[ "authType" ] = req.getAuthType(); 228 | } 229 | 230 | if ( !isNull( req.getClientCert() ) ) { 231 | attrCollection[ "clientCert" ] = req.getClientCert(); 232 | } 233 | 234 | if ( !isNull( req.getClientCertPassword() ) ) { 235 | attrCollection[ "clientCertPassword" ] = req.getClientCertPassword(); 236 | } 237 | 238 | cfhttp( attributeCollection = attrCollection ) { 239 | var headers = req.getHeaders(); 240 | for ( var name in headers ) { 241 | // we want to skip adding a Content-Type header when there are files 242 | // so that the CFML engines can add the correct boundary to the Content-Type 243 | if ( name == "Content-Type" && !req.getFiles().isEmpty() ) { 244 | continue; 245 | } 246 | 247 | cfhttpparam( 248 | type = "header", 249 | name = name, 250 | value = headers[ name ] 251 | ); 252 | } 253 | 254 | var queryParams = req.getQueryParams(); 255 | for ( var param in queryParams ) { 256 | cfhttpparam( 257 | type = "url", 258 | name = param.name, 259 | value = param.value, 260 | encoded = true // this will be ignored by ACF, but used by Lucee and BoxLang (currently) 261 | ); 262 | } 263 | 264 | for ( var file in req.getFiles() ) { 265 | var fileAttrCollection = { 266 | type : "file", 267 | name : file.name, 268 | file : file.path 269 | }; 270 | if ( file.keyExists( "mimeType" ) && !isNull( file.mimeType ) ) { 271 | fileAttrCollection[ "mimeType" ] = file.mimeType; 272 | } 273 | cfhttpparam( attributeCollection = fileAttrCollection ); 274 | } 275 | 276 | var cookies = req.getCookies(); 277 | for ( var name in cookies ) { 278 | cfhttpparam( 279 | type = "cookie", 280 | name = name, 281 | value = cookies[ name ] 282 | ); 283 | } 284 | 285 | if ( req.hasBody() ) { 286 | switch ( req.getBodyFormat() ) { 287 | case "json": 288 | // this is here for backwards compatibility 289 | if ( !headers.keyExists( "Content-Type" ) ) { 290 | cfhttpparam( 291 | type = "header", 292 | name = "Content-Type", 293 | value = "application/json" 294 | ); 295 | } 296 | 297 | cfhttpparam( type = "body", value = req.prepareBody() ); 298 | break; 299 | case "formFields": 300 | var body = req.getBody(); 301 | for ( var fieldName in body ) { 302 | for ( var value in arrayWrap( body[ fieldName ] ) ) { 303 | cfhttpparam( 304 | type = "formfield", 305 | name = fieldName, 306 | value = value 307 | ); 308 | } 309 | } 310 | break; 311 | case "xml": 312 | cfhttpparam( type = "xml", value = req.prepareBody() ); 313 | break; 314 | default: 315 | cfhttpparam( type = "body", value = req.prepareBody() ); 316 | } 317 | } 318 | } 319 | return local.res; 320 | } 321 | 322 | private numeric function normalizeStatusCode( required struct cfhttpResponse ) { 323 | if ( cfhttpResponse.keyExists( "responseheader" ) && cfhttpResponse.responseheader.keyExists( "status_code" ) ) { 324 | return cfhttpResponse.responseheader.status_code; 325 | } 326 | 327 | if ( cfhttpResponse.keyExists( "status_code" ) ) { 328 | return cfhttpResponse.status_code != 0 ? cfhttpResponse.status_code : 502; 329 | } 330 | 331 | if ( cfhttpResponse.keyExists( "statuscode" ) ) { 332 | var code = listFirst( cfhttpResponse.statuscode, " " ); 333 | return isNumeric( code ) ? code : 502; 334 | } 335 | 336 | return 504; 337 | } 338 | 339 | private string function normalizeStatusText( required struct cfhttpResponse ) { 340 | if ( cfhttpResponse.keyExists( "responseheader" ) && cfhttpResponse.responseheader.keyExists( "explanation" ) ) { 341 | return cfhttpResponse.responseheader.explanation; 342 | } 343 | 344 | if ( cfhttpResponse.keyExists( "status_text" ) ) { 345 | if ( cfhttpResponse.status_text == "Request Time-out" ) { 346 | return "Request Timeout"; 347 | } 348 | 349 | if ( cfhttpResponse.status_text == "Connection Failure" ) { 350 | return "Bad Gateway"; 351 | } 352 | 353 | return cfhttpResponse.status_text; 354 | } 355 | 356 | if ( cfhttpResponse.keyExists( "statuscode" ) ) { 357 | var code = listFirst( cfhttpResponse.statuscode, " " ); 358 | if ( !isNumeric( code ) ) { 359 | return "Bad Gateway"; 360 | } 361 | var text = listRest( cfhttpResponse.statuscode, " " ); 362 | return text == "Request Time-out" ? "Request Timeout" : text; 363 | } 364 | 365 | return "Gateway Timeout"; 366 | } 367 | 368 | /** 369 | * Normalizes the headers from a CFHTTP response into a normalized struct. 370 | * 371 | * @cfhttpResponse The cfhttp response struct. 372 | * 373 | * @returns A struct of normalized headers. 374 | */ 375 | private struct function normalizeHeaders( required struct cfhttpResponse ) { 376 | var headers = {}; 377 | cfhttpResponse.responseheader.each( function( name, value ) { 378 | headers[ lCase( name ) ] = value; 379 | } ); 380 | return headers; 381 | } 382 | 383 | /** 384 | * Ensures the return value is an array, either by returning an array 385 | * or by returning the value wrapped in an array. 386 | * 387 | * @value The value to ensure is an array. 388 | * 389 | * @doc_generic any 390 | * @return [any] 391 | */ 392 | private array function arrayWrap( required any value ) { 393 | return isArray( arguments.value ) ? arguments.value : [ arguments.value ]; 394 | } 395 | 396 | } 397 | --------------------------------------------------------------------------------