234 attempt to fix js error Uncaught SyntaxError: Unexpected identifier 'hidden'
This commit is contained in:
@@ -1,80 +1,46 @@
|
||||
<cfcomponent extends="taffy.core.api" output="false">
|
||||
<cfscript>
|
||||
this.name = "Taffy_testSuite";
|
||||
this.dirPath = getDirectoryFromPath(getCurrentTemplatePath());
|
||||
this.mappings["/resources"] = this.dirPath & "resources/";
|
||||
this.mappings["/mxunit"] = this.dirPath & "testbox/system/compat/";
|
||||
this.mappings["/testbox"] = this.dirPath & "testbox/";
|
||||
this.mappings["/bugLog"] = this.dirPath & "BugLogHQ/";
|
||||
this.mappings["/Hoth"] = this.dirPath & "Hoth/";
|
||||
this.mappings["/di1"] = this.dirPath & "di1/";
|
||||
component {
|
||||
|
||||
this.system = createObject("java", "java.lang.System");
|
||||
this.datasources["bugLog"] = {
|
||||
class: (structKeyExists(server, "lucee")) ? 'org.gjt.mm.mysql.Driver' : 'com.mysql.jdbc.Driver',
|
||||
connectionString: 'jdbc:mysql://localhost:3306/buglog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT&useLegacyDatetimeCode=false',
|
||||
url: 'jdbc:mysql://localhost:3306/buglog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT&useLegacyDatetimeCode=false',
|
||||
username: this.system.getenv("DB_USER"),
|
||||
password: this.system.getenv("DB_PASS"),
|
||||
driver: "other"
|
||||
this.name = "TaffyTestHarness_" & hash(getCurrentTemplatePath());
|
||||
this.sessionManagement = false;
|
||||
|
||||
// Set up mappings for tests
|
||||
variables.testsPath = getDirectoryFromPath(getCurrentTemplatePath());
|
||||
// Strip trailing slash before calling getDirectoryFromPath to reliably get parent dir across engines
|
||||
variables.rootPath = getDirectoryFromPath(left(variables.testsPath, len(variables.testsPath) - 1));
|
||||
|
||||
this.mappings["/taffy"] = variables.rootPath;
|
||||
this.mappings["/testbox"] = variables.testsPath & "testbox";
|
||||
this.mappings["/tests"] = variables.testsPath;
|
||||
this.mappings["/resources"] = variables.testsPath & "resources";
|
||||
|
||||
// Also set component paths for Lucee 6 compatibility
|
||||
this.componentPaths = [
|
||||
{
|
||||
"physical": variables.rootPath,
|
||||
"archive": "",
|
||||
"primary": "physical",
|
||||
"inspectTemplate": "always"
|
||||
}
|
||||
];
|
||||
|
||||
function onApplicationStart() {
|
||||
// Initialize minimal Taffy context for tests that need it
|
||||
application._taffy = {
|
||||
settings: {
|
||||
serializer: "core.nativeJsonSerializer",
|
||||
noDataSends204NoContent: false
|
||||
},
|
||||
factory: new core.factory(),
|
||||
compat: {
|
||||
queryToArray: "missing",
|
||||
queryToStruct: "missing"
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
function onRequestStart(targetPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
variables.framework = {};
|
||||
variables.framework.disableDashboard = false;
|
||||
variables.framework.reloadKey = "reload";
|
||||
variables.framework.unhandledPaths = "/Taffy/tests/someFolder,/Taffy/tests/tests,/tests/someFolder,/tests/tests,BugLogHQ";
|
||||
variables.framework.serializer = "nativeJsonSerializer";
|
||||
variables.framework.useEtags = true;
|
||||
variables.framework.globalHeaders = {};
|
||||
variables.framework.globalHeaders["x-foo-globalheader"] = "snafu";
|
||||
variables.framework.exposeHeaders = true;
|
||||
|
||||
variables.framework.environments = {};
|
||||
variables.framework.environments.test = {};
|
||||
variables.framework.environments.test.reloadPassword = 'dontpanic';
|
||||
|
||||
function getCacheKey(cfc, requestArguments, matchedURI) {
|
||||
if(structKeyExists(requestArguments, "default")) {
|
||||
return super.getCacheKey(argumentCollection = arguments);
|
||||
}
|
||||
|
||||
return lCase(arguments.cfc & "_" & listSort(structKeyList(arguments.requestArguments), "textnocase"));
|
||||
}
|
||||
|
||||
function getEnvironment(){
|
||||
return "test";
|
||||
}
|
||||
|
||||
function onRequest(string target) {
|
||||
if (arguments.target contains "/BugLogHQ/") {
|
||||
return true;
|
||||
} else {
|
||||
super.onRequest(arguments.target);
|
||||
}
|
||||
}
|
||||
|
||||
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers, methodMetadata, matchedURI) {
|
||||
var local = {};
|
||||
|
||||
//pass data into a resource by modifying requestArguments
|
||||
if (structKeyExists(arguments.requestArguments, "hulk") and arguments.requestArguments.hulk eq "smash"){
|
||||
arguments.requestArguments.dataFromOTR = "who let the hulk out?!";
|
||||
}
|
||||
|
||||
//get basic auth data, if any, and pass it into the resources
|
||||
local.credentials = getBasicAuthCredentials();
|
||||
arguments.requestArguments.username = local.credentials.username;
|
||||
arguments.requestArguments.password = local.credentials.password;
|
||||
|
||||
if (structKeyExists(arguments.requestArguments, "refuse") and arguments.requestArguments.refuse)
|
||||
{
|
||||
return noData().withStatus(405);
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
</cfscript>
|
||||
</cfcomponent>
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
component {
|
||||
|
||||
/**
|
||||
* Returns a sample query for testing queryToArray/queryToStruct
|
||||
*/
|
||||
function getSampleQuery() {
|
||||
return queryNew(
|
||||
"id,name,email,active",
|
||||
"integer,varchar,varchar,bit",
|
||||
[
|
||||
{ id: 1, name: "John Doe", email: "john@example.com", active: true },
|
||||
{ id: 2, name: "Jane Smith", email: "jane@example.com", active: true },
|
||||
{ id: 3, name: "Bob Wilson", email: "bob@example.com", active: false }
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single-row query for testing queryToStruct
|
||||
*/
|
||||
function getSingleRowQuery() {
|
||||
return queryNew(
|
||||
"id,name,email",
|
||||
"integer,varchar,varchar",
|
||||
[{ id: 1, name: "Test User", email: "test@example.com" }]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sample struct data
|
||||
*/
|
||||
function getSampleStruct() {
|
||||
return {
|
||||
id: 123,
|
||||
name: "Test Item",
|
||||
tags: ["tag1", "tag2"],
|
||||
metadata: {
|
||||
created: now(),
|
||||
version: "1.0"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sample array data
|
||||
*/
|
||||
function getSampleArray() {
|
||||
return [
|
||||
{ id: 1, value: "first" },
|
||||
{ id: 2, value: "second" },
|
||||
{ id: 3, value: "third" }
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
component {
|
||||
|
||||
/**
|
||||
* Walks the metadata chain to check if an object extends a given class.
|
||||
* Works around Lucee's isInstanceOf() bug with mapped paths.
|
||||
*/
|
||||
public boolean function extendsClass(required any obj, required string className) {
|
||||
if (!isObject(arguments.obj)) return false;
|
||||
var meta = getMetadata(arguments.obj);
|
||||
while (structKeyExists(meta, "name")) {
|
||||
if (listLast(meta.name, ".") == listLast(arguments.className, ".")) return true;
|
||||
if (!structKeyExists(meta, "extends")) break;
|
||||
meta = meta.extends;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
/**
|
||||
Aaron Greenlee
|
||||
http://aarongreenlee.com/
|
||||
|
||||
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
|
||||
Unported License.
|
||||
|
||||
// Original Info -----------------------------------------------------------
|
||||
Author : Aaron Greenlee
|
||||
Created : 01/12/2011
|
||||
|
||||
HothReporter offers a simple Web UI to report the errors observed by Hoth.
|
||||
|
||||
// Modifications :---------------------------------------------------------
|
||||
|
||||
*/
|
||||
component
|
||||
name='HothReporter'
|
||||
accessors=false
|
||||
output="false"
|
||||
{
|
||||
public Hoth.HothReporter function init (HothConfig)
|
||||
{
|
||||
// If a config object was not provided we
|
||||
// will use our default.
|
||||
variables.Config = (structKeyExists(arguments, 'HothConfig'))
|
||||
? arguments.HothConfig
|
||||
: new Hoth.config.HothConfig();
|
||||
|
||||
VARIABLES._NAME = 'Hoth_' & variables.Config.getApplicationName();
|
||||
|
||||
variables.exceptionKeys = ['detail','type','tagcontext','stacktrace','message'];// Required exception keys
|
||||
variables.logPathIsRelative = variables.Config.getLogPathIsRelative();
|
||||
variables.paths.LogPath = variables.Config.getLogPathExpanded(); // Get the root location for our logging.
|
||||
variables.paths.Exceptions = variables.Config.getPath('exceptions'); // Track the unique exceptions.
|
||||
variables.paths.Incidents = variables.Config.getPath('incidents'); // Track the hits per exception.
|
||||
variables.paths.Report = variables.Config.getPath('exceptionReport'); // The actual report
|
||||
variables.paths.Activity = variables.Config.getPath('exceptionReportActivity'); // Track when we save things. Helps understand volume.
|
||||
//variables.paths.Index = variables.Config.getPath('exceptionIndex'); // Tracks the exception keys to prevent duplication
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Quick report. Really a work in process.
|
||||
* @exception Accepts a hash value or 'all'
|
||||
**/
|
||||
public struct function report (required string exception)
|
||||
{
|
||||
if (arguments.exception=='all')
|
||||
{
|
||||
return generateExceptionIndex();
|
||||
} else {
|
||||
local.filepath = variables.paths.Exceptions & '/' & arguments.exception;
|
||||
|
||||
local.exception = (fileExists(local.filepath))
|
||||
? fileRead(local.filepath)
|
||||
: serializeJSON ({'message'="That report no longer exists."});
|
||||
|
||||
return deserializeJSON (local.exception);
|
||||
}
|
||||
}
|
||||
|
||||
/** Quick report. Really a work in process.
|
||||
* @exception Accepts a hash value or 'all'
|
||||
**/
|
||||
public array function delete (required string exception)
|
||||
{
|
||||
|
||||
local.response = [];
|
||||
if (arguments.exception == 'all')
|
||||
{
|
||||
lock name=VARIABLES._NAME timeout=variables.Config.getTimeToLock() type="exclusive" {
|
||||
directoryDelete(variables.paths.Exceptions, true);
|
||||
directoryDelete(variables.paths.Incidents, true);
|
||||
directoryCreate(variables.paths.Exceptions);
|
||||
directoryCreate(variables.paths.Incidents);
|
||||
}
|
||||
} else { local.exceptionPath = variables.paths.Exceptions & '/' & arguments.exception;
|
||||
local.incidentPath = variables.paths.Incidents & '/' & arguments.exception;
|
||||
|
||||
local.response = [];
|
||||
|
||||
if (fileExists(local.exceptionPath))
|
||||
{
|
||||
fileDelete(local.exceptionPath);
|
||||
arrayAppend(local.response, "Exception file deleted.");
|
||||
} else {
|
||||
arrayAppend(local.response, "Exception file did not exist!");
|
||||
}
|
||||
if (fileExists(local.incidentPath))
|
||||
{
|
||||
fileDelete(local.incidentPath);
|
||||
arrayAppend(local.response, "Incident record file deleted.");
|
||||
} else {
|
||||
arrayAppend(local.response, "Incident record file not exist!");
|
||||
}
|
||||
return local.response;
|
||||
}
|
||||
}
|
||||
|
||||
/** Return the report's view **/
|
||||
public string function getReportView () {
|
||||
local.view = fileRead(expandPath('/Hoth') & '/views/report.html');
|
||||
|
||||
// Replace the Hoth URL
|
||||
return replaceNoCase(
|
||||
local.view
|
||||
,'${HOTH_REPORT_URL}'
|
||||
,variables.Config.getHothReportURL());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
private struct function generateExceptionIndex() {
|
||||
// Read our file system
|
||||
local.exceptions = directoryList (variables.paths.Exceptions,false);
|
||||
local.incidents = directoryList (variables.paths.Exceptions,false);
|
||||
|
||||
local.report = {};
|
||||
for (i=1;i LTE ArrayLen(local.exceptions);i=i+1) {
|
||||
|
||||
local.instance = {};
|
||||
local.instance.filename =
|
||||
listLast(local.exceptions[i],'\/');
|
||||
|
||||
if (left(local.instance.filename, 1) != '_')
|
||||
{
|
||||
//local.instance.exceptionDetail =
|
||||
//fileRead (local.exceptions[i]);
|
||||
|
||||
if (!fileExists(variables.paths.Incidents & '/' & local.instance.filename))
|
||||
{
|
||||
local.instances = '';
|
||||
} else {
|
||||
local.instances =
|
||||
fileRead(variables.paths.Incidents & '/' & local.instance.filename);
|
||||
}
|
||||
|
||||
local.instance.incidentcount = listLen(local.instances,chr(10));
|
||||
|
||||
// Save our report
|
||||
local.report[local.instance.filename] = local.instance;
|
||||
}
|
||||
}
|
||||
|
||||
return local.report;
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
/**
|
||||
Aaron Greenlee
|
||||
http://aarongreenlee.com/
|
||||
|
||||
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
|
||||
Unported License.
|
||||
|
||||
// Original Info -----------------------------------------------------------
|
||||
Author : Aaron Greenlee
|
||||
Created : 10/01/2010
|
||||
|
||||
HothTracker is responsible for accepting a exception, tracking the frequency
|
||||
of the exception and alerting developers for new, unique exceptions.
|
||||
|
||||
// Modifications :---------------------------------------------------------
|
||||
Modified : 12/13/2010 9:52:41 AM by Aaron Greenlee.
|
||||
- Now supporting ColdBox 3.0 RC1
|
||||
*/
|
||||
component
|
||||
name='HothTracker'
|
||||
accessors=false
|
||||
{
|
||||
|
||||
public Hoth.HothTracker function init (HothConfig) {
|
||||
// If a config object was not provided we
|
||||
// will use our default.
|
||||
variables.Config = (structKeyExists(arguments, 'HothConfig'))
|
||||
? arguments.HothConfig
|
||||
: new Hoth.object.HothConfig();
|
||||
|
||||
VARIABLES._NAME = 'Hoth_' & variables.Config.getApplicationName();
|
||||
|
||||
variables.exceptionKeys = ['detail','type','tagcontext','stacktrace','message']; // Required exception keys
|
||||
variables.paths.LogPath = variables.Config.getLogPathExpanded(); // Get the root location for our logging.
|
||||
variables.paths.Exceptions = variables.Config.getPath('exceptions'); // Track the unique exceptions.
|
||||
variables.paths.Incidents = variables.Config.getPath('incidents'); // Track the hits per exception.
|
||||
variables.paths.Report = variables.Config.getPath('exceptionReport'); // The actual report
|
||||
variables.paths.Activity = variables.Config.getPath('exceptionReportActivity'); // Track when we save things. Helps understand volume.
|
||||
//variables.paths.Index = variables.Config.getPath('exceptionIndex'); // Tracks the exception keys to prevent duplication
|
||||
|
||||
verifyDirectoryStructure();
|
||||
|
||||
try {
|
||||
// For the life of this HothTracker, keep the ApplicationManager.
|
||||
variables.HothApplicationManager = new Hoth.object.HothApplicationManager(HothConfig);
|
||||
// Because the entire application db file is locked, we only want to
|
||||
// learn about this application when the HothTracker is created. The
|
||||
// actual amount of work done within the manager is small, but, it is
|
||||
// does block all other processes for all other Hoth instances, so, we
|
||||
// want to reduce calls to it to this constructor.
|
||||
variables.HothApplicationManager.learnApplication(arguments.HothConfig);
|
||||
} catch (any e) {
|
||||
//writeDump(e);abort;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Track an exception.
|
||||
@ExceptionStructure A ColdFusion cfcatch or a supported object from a Framework or Application. */
|
||||
public boolean function track (any Exception) {
|
||||
local.ExceptionStructure = parseException(arguments.Exception);
|
||||
|
||||
// If we did not parse what we are supposed to
|
||||
// track, we will abort.
|
||||
if (!local.ExceptionStructure.validException)
|
||||
return false;
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
try {
|
||||
// ------------------------------------------------------------------------------
|
||||
local.e = {
|
||||
detail = structKeyExists(local.ExceptionStructure,'detail') ? local.ExceptionStructure.detail : '_noDetail'
|
||||
,message = structKeyExists(local.ExceptionStructure,'message') ? local.ExceptionStructure.message : '_noMessage'
|
||||
,stack = structKeyExists(local.ExceptionStructure,'stacktrace') ? local.ExceptionStructure.stacktrace : '_no_stacktrace'
|
||||
,context = structKeyExists(local.ExceptionStructure,'tagcontext') ? local.ExceptionStructure.tagcontext : '_no_tagcontext'
|
||||
,format = structKeyExists(local.ExceptionStructure,'format') ? local.ExceptionStructure.format: '_no_format'
|
||||
,url = CGI.HTTP_HOST & CGI.path_info
|
||||
,client = CGI.HTTP_USER_AGENT
|
||||
};
|
||||
|
||||
// Generate JSON for hashing
|
||||
local.json = {};
|
||||
local.k = '';
|
||||
for(local.k in local.e)
|
||||
local.json[k] = serializeJSON(local.e[local.k]);
|
||||
|
||||
// Hash a unique key for the content of each property within the exception
|
||||
local.index = {};
|
||||
local.index.stack = ( len(local.e.stack) > 0) ? hash(lcase(local.e.stack),'SHA') : '_no_stack';
|
||||
local.index.key = lcase(local.index.stack);
|
||||
|
||||
local.saveDetails = false;
|
||||
|
||||
// Index the exception, count occurances and save details.
|
||||
// Lock is unique to the exception.
|
||||
lock name=local.index.key timeout=variables.Config.getTimeToLock() type="exclusive" {
|
||||
local.filename = local.index.key & '.log';
|
||||
local.exceptionFile = variables.paths.Exceptions & '/' & local.filename;
|
||||
local.incidentsFile = variables.paths.Incidents & '/' & local.filename;
|
||||
|
||||
local.exceptionIsKnown = fileExists(local.exceptionFile);
|
||||
|
||||
if (!local.exceptionIsKnown)
|
||||
fileWrite(local.exceptionFile ,serializeJSON(local.e),'UTF-8');
|
||||
|
||||
// Create an incident if the file does not exist
|
||||
if (!fileExists(local.incidentsFile)) {
|
||||
fileWrite(local.incidentsFile ,now() & '#chr(13)#','UTF-8');
|
||||
} else {
|
||||
local.file = fileOpen(local.incidentsFile,'append','utf-8');
|
||||
fileWriteLine(local.file, now() & '|' & local.e.url );
|
||||
fileClose(local.file);
|
||||
}
|
||||
}
|
||||
|
||||
// Outside the lock, send mail if requested
|
||||
if (!local.exceptionIsKnown && variables.Config.getEmailNewExceptions() ) {
|
||||
|
||||
local.INetAddress = createObject( 'java', 'java.net.InetAddress' );
|
||||
|
||||
local.url = (len(CGI.QUERY_STRING) > 0)
|
||||
? CGI.http_host & CGI.path_info & '?' & Cgi.QUERY_STRING
|
||||
: CGI.http_host & CGI.path_info;
|
||||
|
||||
local.emailBody = [
|
||||
"Hoth tracked a new exception (" & local.index.key & ")."
|
||||
,"Message: " & local.e.message
|
||||
,"Machine Name: " & local.INetAddress.getLocalHost().getHostName()
|
||||
,"Address: " & local.url
|
||||
];
|
||||
|
||||
local.Mail = new Mail( subject='Hoth Exception (' & variables.Config.getApplicationName() & ') ' & local.index.key
|
||||
,to=variables.Config.getEmailNewExceptionsTo()
|
||||
,from=variables.Config.getEmailNewExceptionsFrom());
|
||||
|
||||
// Attach the file
|
||||
if ( variables.Config.getEmailNewExceptionsFile() ) {
|
||||
local.Mail.addParam(file=local.exceptionFile);
|
||||
ArrayAppend( local.emailBody, "To view the attached exception info, copy and paste into FireBug's console (x = exception) and press CRTL+Enter." );
|
||||
}
|
||||
|
||||
// Show exception as HTML inline?
|
||||
if ( variables.Config.getEmailExceptionsAsHTML() ) {
|
||||
local.Mail.setType( "html" );
|
||||
savecontent variable="local.emailBody" {
|
||||
writeOutput( arrayToList( local.emailBody, "<br />" ) );
|
||||
writeOutput( "<br />Exception Details:<br />");
|
||||
writeDump( local.e );
|
||||
}
|
||||
local.Mail.setBody( local.emailBody );
|
||||
}
|
||||
else {
|
||||
local.Mail.setBody( arrayToList( local.emailBody, "#chr(10)##chr(13)#" ) );
|
||||
}
|
||||
|
||||
local.mail.Send();
|
||||
}
|
||||
// ------------------------------------------------------------------------------
|
||||
} catch (any e) {
|
||||
return false;
|
||||
}
|
||||
// ------------------------------------------------------------------------------
|
||||
return true;
|
||||
}
|
||||
|
||||
// Private Methods Follow -------------------------------------------------------
|
||||
/** Parse an exception provided by a framework or supported application.
|
||||
Hoth is easy to support. Just provide a Struct with at least the following keys:
|
||||
'detail,type,tagContext,StackTrace,Message'
|
||||
Or an object with the same information that Hoth can extract. */
|
||||
private struct function parseException(any Exception) {
|
||||
local.result = { validException = false };
|
||||
|
||||
// Return the meta data for our passed exception
|
||||
local.md = getMetaData(Exception);
|
||||
|
||||
// Inspect the class tree to understand what type of object
|
||||
// we are working with.
|
||||
local.classTree = GetClassHeirarchy(local.md);
|
||||
|
||||
// Was our object built with ColdFusion? If so, it is custom.
|
||||
// If not, the object is considered to be native and generated by CF.
|
||||
local.exceptionType = (
|
||||
listContainsNoCase(local.classTree, 'coldfusion.runtime.AttributeCollection', ' ')
|
||||
) ? 'Custom' : 'Native';
|
||||
|
||||
if (local.exceptionType == 'Native')
|
||||
{
|
||||
local.result.detail = (structKeyExists(arguments.Exception, 'detail')) ? arguments.Exception.detail : 'undefined';
|
||||
local.result.message = (structKeyExists(arguments.Exception, 'message')) ? arguments.Exception.message : 'undefined';
|
||||
local.result.stacktrace = (structKeyExists(arguments.Exception, 'stacktrace')) ? arguments.Exception.stacktrace : 'undefined';
|
||||
local.result.tagcontext = (structKeyExists(arguments.Exception, 'tagContext')) ? arguments.Exception.tagContext : 'undefined';
|
||||
local.result.validException = true;
|
||||
local.result.format = 'Native';
|
||||
|
||||
// ADDED by Benoit Hediard to get real detail and message in FW1
|
||||
if (local.result.message == "Event handler exception." && structKeyExists(arguments.Exception, "Cause")) {
|
||||
local.result.detail = (structKeyExists(arguments.Exception.Cause, 'detail')) ? arguments.Exception.Cause.detail : 'undefined';
|
||||
local.result.message = (structKeyExists(arguments.Exception.Cause, 'message')) ? arguments.Exception.Cause.message : 'undefined';
|
||||
};
|
||||
return local.result;
|
||||
} else {
|
||||
//detail,type,tagcontext,stacktrace,message
|
||||
switch(local.md.fullname) {
|
||||
// ColdBox Excepiont
|
||||
case 'coldbox.system.beans.ExceptionBean' :
|
||||
case 'coldbox.system.web.context.ExceptionBean' :
|
||||
local.result.detail = arguments.Exception.getDetail();
|
||||
local.result.message = arguments.Exception.getMessage();
|
||||
local.result.stacktrace = arguments.Exception.getStackTrace();
|
||||
local.result.tagcontext = arguments.Exception.getTagContext();
|
||||
local.result.validException = true;
|
||||
local.result.format = 'ColdBox';
|
||||
break;
|
||||
// Unknown Exception Passed
|
||||
default :
|
||||
local.result.validException = false;
|
||||
local.result.format = 'Unknown';
|
||||
break;
|
||||
/*case 'MachII.util.Exception' :
|
||||
local.result = arguments.Exception.getCaughtException();
|
||||
local.result.validException = true;
|
||||
break;*/
|
||||
}
|
||||
}
|
||||
|
||||
return local.result;
|
||||
}
|
||||
|
||||
/** Inspect the class heirarchy to return a string showing the class tree.
|
||||
Original code is borrowed from Dominic Watson at the following address.
|
||||
Rewritten for for CF9+/Railo features.
|
||||
http://fusion.dominicwatson.co.uk/2007/09/coldfusion-objects-are-java-objects.html **/
|
||||
private function GetClassHeirarchy(obj) {
|
||||
local.thisClass = obj.GetClass();
|
||||
local.sReturn = thisClass.GetName();
|
||||
|
||||
do{
|
||||
local.thisClass = local.thisClass.GetSuperClass();
|
||||
local.sReturn &= " " & thisClass.GetName();
|
||||
} while(CompareNoCase(thisClass.GetName(), 'java.lang.Object'));
|
||||
|
||||
return local.sReturn;
|
||||
}
|
||||
|
||||
/** Verify the desired directory structure exsits. */
|
||||
private void function verifyDirectoryStructure() {
|
||||
// Verify our index diectory exists
|
||||
|
||||
/* Ensure our directory structure is as expected. */
|
||||
lock name=VARIABLES._NAME timeout=variables.Config.getTimeToLock() type="exclusive" {
|
||||
if (!directoryExists(variables.paths.Exceptions)) {
|
||||
directoryCreate(variables.paths.Exceptions);
|
||||
fileWrite(variables.paths.Exceptions & '/_readme.txt','Hoth: The files within this directory contain the complete details for each unique exception.');
|
||||
}
|
||||
if (!directoryExists(variables.paths.Incidents)) {
|
||||
directoryCreate(variables.paths.Incidents);
|
||||
fileWrite(variables.paths.Incidents & '/_readme.txt','Hoth: The files within this directory contain the details about the volume of errors for each unique exception.');
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// This CFC offers no protection--thus, anyone can access this.
|
||||
// You can implement this by renaming the CFC with a UUID--that can secure
|
||||
// this through obscurity. But, I prefer the approach shown in the ColdBox
|
||||
// example. That is what I use. I have not tried this code :(
|
||||
|
||||
component
|
||||
{
|
||||
/** Loads the Web UI (HTML) **/
|
||||
remote function index () returnformat='plain' {
|
||||
local.HothReport = new Hoth.HothReporter( new config.HothConfig() );
|
||||
return local.HothReport.getReportView();
|
||||
}
|
||||
|
||||
/** Access Hoth report data as JSON.
|
||||
@exception If not provided a list of exceptions will be returned.
|
||||
If provided, the value should be an exception hash which
|
||||
modified the behavior to return information for only
|
||||
that exception. **/
|
||||
remote function report (string exception) returnformat='JSON' {
|
||||
local.report = (structKeyExists(arguments, 'exception')
|
||||
? arguments.exception
|
||||
: 'all');
|
||||
|
||||
local.HothReport = new Hoth.HothReporter( new config.HothConfig() );
|
||||
return local.HothReport.report(local.report);
|
||||
}
|
||||
|
||||
/** Delete a report. **/
|
||||
remote function delete (string exception)returnformat='JSON' {
|
||||
if (!structKeyExists(arguments, 'exception'))
|
||||
{
|
||||
// We can delete all exceptions at once!
|
||||
arguments.exception = 'all';
|
||||
}
|
||||
|
||||
local.HothReport = new Hoth.HothReporter( new config.HothConfig() );
|
||||
|
||||
// Delete!
|
||||
return local.HothReporter.delete(arguments.exception);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
Aaron Greenlee
|
||||
http://aarongreenlee.com/
|
||||
|
||||
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
|
||||
Unported License.
|
||||
|
||||
// Original Info -----------------------------------------------------------
|
||||
Author : Aaron Greenlee
|
||||
Created : 10/01/2010
|
||||
|
||||
Default configuration for Hoth.
|
||||
|
||||
*/
|
||||
component
|
||||
implements='Hoth.object.iHothConfig'
|
||||
extends='Hoth.object.CoreConfig'
|
||||
accessors=true {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// BASIC HOTH SETTINGS (required)
|
||||
// -------------------------------------------------------------------------
|
||||
/** What is the name of your application? */
|
||||
property name='applicationName' default='HothDefaultConfig';
|
||||
|
||||
/** How many seconds should we lock file operations?
|
||||
For most operations this is exclusive to a unique exception. */
|
||||
property name='timeToLock' default='1';
|
||||
|
||||
/** Where would you like Hoth to save exception data?
|
||||
This folder should be empty when you start. */
|
||||
property name='logPath' default='/Hoth/examples/Example_Logs';
|
||||
|
||||
/** Is the log file location relative to the webroot?
|
||||
This folder should be empty when you start. */
|
||||
property name='logPathIsRelative' default='true';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// HOTH EMAIL SETTINGS (required)
|
||||
// -------------------------------------------------------------------------
|
||||
/** Would you like new exceptions to be emailed to you? */
|
||||
property name='EmailNewExceptions' default='false';
|
||||
|
||||
/** What address(es) should receive these e-mails? */
|
||||
property name='EmailNewExceptionsTo' default='you@yourdomain.com';
|
||||
|
||||
/** What address would you like these emails sent from? */
|
||||
property name='EmailNewExceptionsFrom' default='hoth@yourdomain.com';
|
||||
|
||||
/** Would you like the raw JSON attached to the e-mail? */
|
||||
property name='EmailNewExceptionsFile' default='false';
|
||||
|
||||
/** Would you like HTML emails which contain the exception? */
|
||||
property name='EmailExceptionsAsHTML' default='false';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// HOTH REPORT SETTINGS (required)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** How you access the Hoth reports is up to you. When the reports are
|
||||
generated Hoth needs to know how to build links so you can navigate
|
||||
the report and get information from the server.
|
||||
|
||||
You are responsible for deciding how you wish to access reports. Once
|
||||
you have figured that out, please, write the full URL here.
|
||||
**/
|
||||
property
|
||||
name='HothReportURL'
|
||||
default='http://office/lib/Hoth/examples/ColdFusion/HothReportUI.cfc';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
public function init()
|
||||
{
|
||||
super.init();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/*.hoth
|
||||
@@ -1,3 +0,0 @@
|
||||
This folder is populated by Hoth to keep track of all applications that
|
||||
use this instance of Hoth so Hoth can find the log files for your
|
||||
applications when generating reports.
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
--------------------------------------------------------------------
|
||||
I configure and load Hoth into the ColdBox cache
|
||||
--------------------------------------------------------------------
|
||||
Define settings in ColdBox.cfc for example:
|
||||
interceptors = [
|
||||
//Autowire
|
||||
{
|
||||
class="coldbox.system.interceptors.Autowire",
|
||||
properties={}
|
||||
},
|
||||
//SES
|
||||
{
|
||||
class="coldbox.system.interceptors.SES",
|
||||
properties={}
|
||||
},
|
||||
//HothTracker
|
||||
{
|
||||
class="hoth.extras.ColdBoxInterceptor",
|
||||
properties={
|
||||
EmailNewExceptions = true,
|
||||
EmailNewExceptionsFile = true,
|
||||
EmailNewExceptionsTo = "kaboom@mysite.com",
|
||||
EmailNewExceptionsFrom = "server@mysite.com"
|
||||
}
|
||||
}
|
||||
];
|
||||
--------------------------------------------------------------------
|
||||
*/
|
||||
component
|
||||
{
|
||||
|
||||
/* CONSTRUCTOR
|
||||
------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This is the configuration method for the interceptor
|
||||
*/
|
||||
void function Configure()
|
||||
{
|
||||
// check for required settings
|
||||
if ( !propertyExists( "EmailNewExceptionsTo" ) )
|
||||
{
|
||||
getPlugin( "logger" ).fatal( "hoth.extras.ColdBoxInterceptor.Configure", "The required 'EmailNewExceptionsTo' property has not been defined" );
|
||||
throw(
|
||||
type="hoth.extras.ColdBoxInterceptor"
|
||||
, message="The required 'EmailNewExceptionsTo' property has not been defined"
|
||||
);
|
||||
}
|
||||
|
||||
// Use ColdBox settings if not specified
|
||||
if ( !propertyExists( "applicationName" ) )
|
||||
{
|
||||
setProperty( "applicationName", getSetting( "AppName" ) & " (" & getSetting( "Environment" ) & ")" );
|
||||
}
|
||||
if ( !propertyExists( "logPath" ) )
|
||||
{
|
||||
setProperty( "logPath", getSetting( "ApplicationPath" ) & "logs/hoth" );
|
||||
setProperty( "logPathIsRelative", false );
|
||||
}
|
||||
|
||||
// Optional settings
|
||||
if ( !propertyExists( "cacheKeyName" ) )
|
||||
{
|
||||
setProperty( "cacheKeyName", "hothtracker" );
|
||||
}
|
||||
}
|
||||
|
||||
/* INTERCEPTION POINTS
|
||||
------------------------------------------- */
|
||||
|
||||
void function afterAspectsLoad( required any event, required struct interceptData )
|
||||
{
|
||||
var HothConfig = "";
|
||||
var HothTracker = "";
|
||||
var key = "";
|
||||
var properties = getProperties();
|
||||
|
||||
HothConfig = new hoth.config.HothConfig();
|
||||
|
||||
for ( key in properties )
|
||||
{
|
||||
// use evaluate to dynamically called setters
|
||||
if ( StructKeyExists( HothConfig, "set" & key ) )
|
||||
{
|
||||
evaluate( "HothConfig.set#key#( properties[ key ] )" );
|
||||
}
|
||||
}
|
||||
|
||||
HothTracker = new Hoth.HothTracker( HothConfig );
|
||||
|
||||
// store in ColdBox Cache
|
||||
getColdboxOCM().set( getProperty( 'cacheKeyName' ), HothTracker, 0 );
|
||||
|
||||
getPlugin( "logger" ).info( "hoth.extras.ColdBoxInterceptor", "HothTracker configured and loaded in ColdBox cache using key '#getProperty( 'cacheKeyName' )#'" );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
This is the default location for Hoth logs.
|
||||
|
||||
You should provide a better location in your Hoth Config object that
|
||||
is unique to your application.
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
Aaron Greenlee
|
||||
http://aarongreenlee.com/
|
||||
|
||||
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
|
||||
Unported License.
|
||||
|
||||
// Original Info -----------------------------------------------------------
|
||||
Author : Aaron Greenlee
|
||||
Created : 10/01/2010
|
||||
|
||||
Core configuration object for Hoth. This object provides all the
|
||||
functionality for a Hoth Config Object. Individual implementations should
|
||||
extend this object and provide their own values within the property
|
||||
declaration.
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
Modified : 12/13/2010 10:04:06 AM by Aaron Greenlee.
|
||||
- Added support for global settings.
|
||||
|
||||
*/
|
||||
component
|
||||
accessors='true'
|
||||
{
|
||||
|
||||
/** Construct a configuration object for Hoth. */
|
||||
public function init ()
|
||||
{
|
||||
local.md = getMetadata(this);
|
||||
local.n = arrayLen(local.md.properties);
|
||||
for (local.i=1; local.i <= local.n; local.i++) {
|
||||
local.fn = this['set' & local.md.properties[local.i].name];
|
||||
local.fn(local.md.properties[local.i]['default']);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void function setGlobalDatabasePath(required string path) {
|
||||
variables.GlobalDatabasePath = arguments.path;
|
||||
}
|
||||
public string function getGlobalDatabasePath() {
|
||||
return (structKeyExists(variables, 'GlobalDatabasePath'))
|
||||
? variables.GlobalDatabasePath
|
||||
: '/Hoth/db/';
|
||||
}
|
||||
|
||||
/** Expands a path **/
|
||||
public string function getLogPathExpanded ()
|
||||
{
|
||||
if (!structKeyExists(variables, 'logPathIsRelative')) {
|
||||
variables.logPathIsRelative = false;
|
||||
}
|
||||
return variables.logPathIsRelative ? expandPath( getLogPath() ) : getLogPath();
|
||||
}
|
||||
|
||||
/** Return a path for Hoth. */
|
||||
public string function getPath (name)
|
||||
{
|
||||
switch (arguments.name) {
|
||||
// Sub-Directories
|
||||
case 'incidents' :
|
||||
case 'exceptions' :
|
||||
return getLogPathExpanded() & '/' & lcase(arguments.name);
|
||||
break;
|
||||
|
||||
// Files
|
||||
case 'exceptionIndex' :
|
||||
case 'exceptionReport' :
|
||||
case 'exceptionReportActivity' :
|
||||
return getLogPathExpanded() & '/' & lcase(arguments.name) & '.log';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/**
|
||||
Aaron Greenlee
|
||||
http://aarongreenlee.com/
|
||||
|
||||
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
|
||||
Unported License.
|
||||
|
||||
// Original Info -----------------------------------------------------------
|
||||
Author : Aaron Greenlee
|
||||
Created : 12/13/2010
|
||||
|
||||
Responsible for helping Hoth report for multiple applications.
|
||||
*/
|
||||
component
|
||||
name = 'HothApplicationManager'
|
||||
accessors = true
|
||||
{
|
||||
property name='applications' type='array';
|
||||
|
||||
// The filename that holds application knowledge.
|
||||
VARIABLES.APP_LOCK_KEY = 'HothApplicationDBFileLock';
|
||||
|
||||
/** Constructor.
|
||||
* @HothConfig Requires a HothConfig Object.
|
||||
*/
|
||||
public Hoth.object.HothApplicationManager function init(
|
||||
required HothConfig
|
||||
){
|
||||
|
||||
setApplications( loadApplicationsFromDisk(HothConfig) );
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** To allow for easy reporting, Hoth needs to be able to track new
|
||||
applications (or really, new log paths). When a new instance of
|
||||
HothTracker is created, this method will be called. **/
|
||||
public boolean function learnApplication(
|
||||
required HothConfig
|
||||
){
|
||||
// Prevent any other threads from saving our new application
|
||||
lock
|
||||
name=VARIABLES.APP_LOCK_KEY
|
||||
timeout=arguments.HothConfig.getTimeToLock()
|
||||
type="exclusive"
|
||||
{
|
||||
local.knownApplications =
|
||||
loadApplicationsFromDisk(argumentCollection=arguments);
|
||||
|
||||
for(local.index in local.knownApplications)
|
||||
{
|
||||
// Confirm our keys exist
|
||||
if (structKeyExists(local.index, 'logPath'))
|
||||
{
|
||||
// If we find the same logPath, we know about this
|
||||
// application and need to exit.
|
||||
if (local.index.logPath == arguments.HothConfig.getLogPath())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We did not find out application path within the file.
|
||||
// Create the struct and append it to our array.
|
||||
local.applicationDataToSave =
|
||||
{
|
||||
'applicationName' = arguments.HothConfig.getApplicationName()
|
||||
,'logPath' = arguments.HothConfig.getLogPath()
|
||||
,'created' = now()
|
||||
};
|
||||
arrayAppend(
|
||||
local.knownApplications
|
||||
,local.applicationDataToSave
|
||||
);
|
||||
|
||||
// Save the contents of the entire array, replacing the entire file
|
||||
local.path = arguments.HothConfig.getGlobalDatabasePath();
|
||||
fileWrite(
|
||||
expandPath(arguments.HothConfig.getGlobalDatabasePath() & 'applications.hoth')
|
||||
,serializeJSON(local.knownApplications)
|
||||
,'UTF-8'
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Loads applications already observed by Hoth from disk.
|
||||
* This process should be locked by the calling method.
|
||||
* @HothConfig Requires a HothConfig Object. */
|
||||
private array function loadApplicationsFromDisk (
|
||||
required HothConfig
|
||||
){
|
||||
local.path = arguments.HothConfig.getGlobalDatabasePath();
|
||||
local.appfile = expandPath(arguments.HothConfig.getGlobalDatabasePath() & 'applications.hoth');
|
||||
|
||||
// Return an empty array if the appFile does not exist.
|
||||
if (!fileExists( local.appfile ) )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
local.applicationsFound = fileRead( local.appfile );
|
||||
|
||||
// Delete the database if the file is invalid.
|
||||
if (!isJSON(local.applicationsFound))
|
||||
{
|
||||
fileDelete( local.appfile );
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert the file to our desired structure
|
||||
local.parsedDB = deserializeJSON(local.applicationsFound);
|
||||
|
||||
return local.parsedDB;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
component
|
||||
{
|
||||
/**
|
||||
* Returns the number of seconds since UTC January 1, 1970, 00:00:00
|
||||
* (Epoch time).
|
||||
*
|
||||
* @param DateTime Date/time object you want converted to
|
||||
* Epoch time.(Required)
|
||||
*
|
||||
* @return Returns a numeric value.
|
||||
* @author Rob Brooks-Bilson (rbils@amkor.com)
|
||||
* @version 1, June 21, 2002
|
||||
*/
|
||||
function GetEpochTimeFromLocal() {
|
||||
local.datetime = 0;
|
||||
|
||||
if ( arrayLen(arguments) == 0)
|
||||
{
|
||||
local.datetime = Now();
|
||||
} else {
|
||||
local.datetime = arguments[1];
|
||||
}
|
||||
|
||||
return
|
||||
DateDiff(
|
||||
"s"
|
||||
,DateConvert("utc2Local", "January 1 1970 00:00")
|
||||
,datetime);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
Aaron Greenlee
|
||||
http://aarongreenlee.com/
|
||||
|
||||
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
|
||||
Unported License.
|
||||
|
||||
// Original Info -----------------------------------------------------------
|
||||
Author : Aaron Greenlee
|
||||
Created : 10/01/2010
|
||||
|
||||
Interface for a Hoth Config Object.
|
||||
|
||||
*/
|
||||
interface {
|
||||
public function init ();
|
||||
public string function getLogPathExpanded();
|
||||
public string function getPath (name);
|
||||
}
|
||||
@@ -1,457 +0,0 @@
|
||||
<cfcomponent displayname="bugLogListener">
|
||||
|
||||
<!---
|
||||
bugLogListener.cfc
|
||||
|
||||
This is the main point of entry into the bugLog API. This component is the one that
|
||||
actually processes the bug reports, it adds them to the database and is responsible
|
||||
for processing any defined rules.
|
||||
|
||||
Created: 2007 - Oscar Arevalo - http://www.oscararevalo.com
|
||||
--->
|
||||
|
||||
<cfset variables.startedOn = 0>
|
||||
<cfset variables.oDAOFactory = 0>
|
||||
<cfset variables.oRuleProcessor = 0>
|
||||
<cfset variables.oConfig = 0>
|
||||
<cfset variables.msgLog = arrayNew(1)>
|
||||
<cfset variables.maxLogSize = 10>
|
||||
<cfset variables.instanceName = "">
|
||||
<cfset variables.autoCreateDefault = true>
|
||||
|
||||
<cffunction name="init" access="public" returntype="bugLogListener" hint="This is the constructor">
|
||||
<cfargument name="config" required="true" type="config">
|
||||
<cfargument name="instanceName" type="string" required="true">
|
||||
<cfscript>
|
||||
var cacheTTL = 300; // timeout in minutes for cache entries
|
||||
|
||||
variables.oConfig = arguments.config; // global configuration
|
||||
variables.instanceName = arguments.instanceName;
|
||||
|
||||
logMessage("Starting BugLogListener (#instanceName#) service...");
|
||||
|
||||
// load settings
|
||||
variables.maxLogSize = arguments.config.getSetting("service.maxLogSize");
|
||||
|
||||
// load DAO Factory
|
||||
variables.oDAOFactory = createObject("component","bugLog.components.DAOFactory").init( variables.oConfig );
|
||||
|
||||
// load the finder objects
|
||||
variables.oAppFinder = createObject("component","bugLog.components.appFinder").init( variables.oDAOFactory.getDAO("application") );
|
||||
variables.oHostFinder = createObject("component","bugLog.components.hostFinder").init( variables.oDAOFactory.getDAO("host") );
|
||||
variables.oSeverityFinder = createObject("component","bugLog.components.severityFinder").init( variables.oDAOFactory.getDAO("severity") );
|
||||
variables.oSourceFinder = createObject("component","bugLog.components.sourceFinder").init( variables.oDAOFactory.getDAO("source") );
|
||||
variables.oUserFinder = createObject("component","bugLog.components.userFinder").init( variables.oDAOFactory.getDAO("user") );
|
||||
|
||||
// load the rule processor
|
||||
variables.oRuleProcessor = createObject("component","bugLog.components.ruleProcessor").init();
|
||||
|
||||
// create cache instances
|
||||
variables.oAppCache = createObject("component","bugLog.components.lib.cache.cacheService").init(50, cacheTTL, false);
|
||||
variables.oHostCache = createObject("component","bugLog.components.lib.cache.cacheService").init(50, cacheTTL, false);
|
||||
variables.oSeverityCache = createObject("component","bugLog.components.lib.cache.cacheService").init(10, cacheTTL, false);
|
||||
variables.oSourceCache = createObject("component","bugLog.components.lib.cache.cacheService").init(5, cacheTTL, false);
|
||||
variables.oUserCache = createObject("component","bugLog.components.lib.cache.cacheService").init(50, cacheTTL, false);
|
||||
|
||||
// load scheduler
|
||||
variables.scheduler = createObject("component","bugLog.components.schedulerService").init( variables.oConfig, variables.instanceName );
|
||||
|
||||
// load the mailer service
|
||||
variables.mailerService = createObject("component","bugLog.components.MailerService").init( variables.oConfig );
|
||||
|
||||
// load rules
|
||||
loadRules();
|
||||
|
||||
// configure history purging
|
||||
configureHistoryPurging();
|
||||
|
||||
// configure the digest sender
|
||||
configureDigest();
|
||||
|
||||
// record the date at which the service started
|
||||
variables.startedOn = Now();
|
||||
|
||||
logMessage("BugLogListener Service (#instanceName#) Started");
|
||||
</cfscript>
|
||||
|
||||
<cfreturn this>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="logEntry" access="public" returntype="void" hint="This method adds a bug report entry into BugLog. Bug reports must be passed as RawEntryBeans">
|
||||
<cfargument name="entryBean" type="rawEntryBean" required="true">
|
||||
|
||||
<cfscript>
|
||||
var bean = arguments.entryBean;
|
||||
var oEntry = 0;
|
||||
var oApp = 0;
|
||||
var oHost = 0;
|
||||
var oSeverity = 0;
|
||||
var oSource = 0;
|
||||
var oDF = variables.oDAOFactory;
|
||||
|
||||
// get autocreate settings
|
||||
var autoCreateApp = allowAutoCreate("application");
|
||||
var autoCreateHost = allowAutoCreate("host");
|
||||
var autoCreateSeverity = allowAutoCreate("severity");
|
||||
var autoCreateSource = allowAutoCreate("source");
|
||||
|
||||
// extract related objects from bean
|
||||
oApp = getApplicationFromBean( bean, autoCreateApp );
|
||||
oHost = getHostFromBean( bean, autoCreateHost );
|
||||
oSeverity = getSeverityFromBean( bean, autoCreateSeverity );
|
||||
oSource = getSourceFromBean( bean, autoCreateSource );
|
||||
|
||||
// create entry
|
||||
oEntry = createObject("component","bugLog.components.entry").init( oDF.getDAO("entry") );
|
||||
oEntry.setDateTime(bean.getdateTime());
|
||||
oEntry.setMessage(bean.getmessage());
|
||||
oEntry.setApplicationID(oApp.getApplicationID());
|
||||
oEntry.setSourceID(oSource.getSourceID());
|
||||
oEntry.setSeverityID(oSeverity.getSeverityID());
|
||||
oEntry.setHostID(oHost.getHostID());
|
||||
oEntry.setExceptionMessage(bean.getexceptionMessage());
|
||||
oEntry.setExceptionDetails(bean.getexceptionDetails());
|
||||
oEntry.setCFID(bean.getcfid());
|
||||
oEntry.setCFTOKEN(bean.getcftoken());
|
||||
oEntry.setUserAgent(bean.getuserAgent());
|
||||
oEntry.setTemplatePath(bean.gettemplate_Path());
|
||||
oEntry.setHTMLReport(bean.getHTMLReport());
|
||||
oEntry.setCreatedOn(bean.getReceivedOn());
|
||||
|
||||
// save entry
|
||||
oEntry.save();
|
||||
|
||||
// process rules
|
||||
variables.oRuleProcessor.processRules(bean, oEntry);
|
||||
|
||||
</cfscript>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getStartedOn" access="public" returntype="date" hint="Returns the date and time where this instance of BugLogListener was created">
|
||||
<cfreturn variables.startedOn>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="shutDown" access="public" returntype="void" hint="Performs any clean up action required">
|
||||
<cfset logMessage("BugLogListener service stopped.")>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="logMessage" access="public" output="false" returntype="void" hint="this method appends an entry to the messages log as well as displays the message on the console">
|
||||
<cfargument name="msg" type="string" required="true" />
|
||||
<cfset var System = CreateObject('java','java.lang.System') />
|
||||
<cfset var txt = timeFormat(now(), 'HH:mm:ss') & ": " & msg>
|
||||
<cfset System.out.println("BugLogListener: " & txt) />
|
||||
<cflock name="bugLogListener_logMessage" type="exclusive" timeout="10">
|
||||
<cfif arrayLen(variables.msgLog) gt variables.maxLogSize>
|
||||
<cfset arrayDeleteAt(variables.msgLog, ArrayLen(variables.msgLog))>
|
||||
</cfif>
|
||||
<cfset arrayPrepend(variables.msgLog,txt)>
|
||||
</cflock>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getConfig" returnType="any" access="public" hint="returns the config settings">
|
||||
<cfreturn variables.oConfig>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getInstanceName" returnType="any" access="public" hint="returns the current instance name">
|
||||
<cfreturn variables.instanceName>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="validate" returntype="boolean" access="public" hint="Validates that a bug report is valid, if not throws an error. This only applies when the requireAPIKey setting is true, otherwise returns True always">
|
||||
<cfargument name="entryBean" type="rawEntryBean" required="true">
|
||||
<cfargument name="apiKey" type="string" required="true">
|
||||
<cfscript>
|
||||
// validate API
|
||||
if(getConfig().getSetting("service.requireAPIKey",false)) {
|
||||
if(arguments.apiKey eq "") {
|
||||
throw(message="Invalid API Key",type="bugLog.invalidAPIKey");
|
||||
}
|
||||
var masterKey = getConfig().getSetting("service.APIKey");
|
||||
if(arguments.apiKey neq masterKey) {
|
||||
var user = getUserByAPIKey(arguments.apiKey);
|
||||
if(!user.getIsAdmin() and arrayLen(user.getAllowedApplications())) {
|
||||
// key is good, but since the user is a non-admin
|
||||
// we need to validate the user can post bugs to the requested
|
||||
// application.
|
||||
var app = getApplicationFromBean( entryBean, false );
|
||||
if(!user.isApplicationAllowed(app)) {
|
||||
throw(message="Application not allowed",type="applicationNotAllowed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate application
|
||||
if(!allowAutoCreate("application")) {
|
||||
getApplicationFromBean( entryBean, false );
|
||||
}
|
||||
|
||||
// validate host
|
||||
if(!allowAutoCreate("host")) {
|
||||
getHostFromBean( entryBean, false );
|
||||
}
|
||||
|
||||
// validte severity
|
||||
if(!allowAutoCreate("severity")) {
|
||||
getSeverityFromBean( entryBean, false );
|
||||
}
|
||||
|
||||
// validate source
|
||||
if(!allowAutoCreate("source")) {
|
||||
getSourceFromBean( entryBean, false );
|
||||
}
|
||||
|
||||
return true;
|
||||
</cfscript>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="reloadRules" access="public" returntype="void" hint="Reloads all rules">
|
||||
<cfset loadRules()>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getMessageLog" access="public" returntype="array">
|
||||
<cfreturn variables.msgLog>
|
||||
</cffunction>
|
||||
|
||||
<!---- Private Methods ---->
|
||||
|
||||
<cffunction name="getApplicationFromBean" access="private" returntype="app" hint="Uses the information on the rawEntryBean to retrieve the corresponding Application object">
|
||||
<cfargument name="entryBean" type="rawEntryBean" required="true">
|
||||
<cfargument name="createIfNeeded" type="boolean" default="false">
|
||||
<cfscript>
|
||||
var key = "";
|
||||
var bean = arguments.entryBean;
|
||||
var oApp = 0;
|
||||
var oDF = variables.oDAOFactory;
|
||||
|
||||
key = bean.getApplicationCode();
|
||||
try {
|
||||
// first we try to get it from the cache
|
||||
oApp = variables.oAppCache.retrieve( key );
|
||||
|
||||
} catch(cacheService.itemNotFound e) {
|
||||
// entry not in cache, so we get it from DB
|
||||
try {
|
||||
oApp = variables.oAppFinder.findByCode( key );
|
||||
|
||||
} catch(appFinderException.ApplicationCodeNotFound e) {
|
||||
// code does not exist, so we need to create it (if autocreate enabled)
|
||||
if(!arguments.createIfNeeded) throw(message="Invalid Application",type="invalidApplication");
|
||||
oApp = createObject("component","bugLog.components.app").init( oDF.getDAO("application") );
|
||||
oApp.setCode( key );
|
||||
oApp.setName( key );
|
||||
oApp.save();
|
||||
}
|
||||
|
||||
// store entry in cache
|
||||
variables.oAppCache.store( key, oApp );
|
||||
}
|
||||
</cfscript>
|
||||
<cfreturn oApp>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getHostFromBean" access="private" returntype="host" hint="Uses the information on the rawEntryBean to retrieve the corresponding Host object">
|
||||
<cfargument name="entryBean" type="rawEntryBean" required="true">
|
||||
<cfargument name="createIfNeeded" type="boolean" default="false">
|
||||
<cfscript>
|
||||
var key = "";
|
||||
var bean = arguments.entryBean;
|
||||
var oHost = 0;
|
||||
var oDF = variables.oDAOFactory;
|
||||
|
||||
key = bean.getHostName();
|
||||
|
||||
try {
|
||||
// first we try to get it from the cache
|
||||
oHost = variables.oHostCache.retrieve( key );
|
||||
|
||||
} catch(cacheService.itemNotFound e) {
|
||||
// entry not in cache, so we get it from DB
|
||||
try {
|
||||
oHost = variables.oHostFinder.findByName( key );
|
||||
|
||||
} catch(hostFinderException.HostNameNotFound e) {
|
||||
// code does not exist, so we need to create it (if autocreate enabled)
|
||||
if(!arguments.createIfNeeded) throw(message="Invalid Host",type="invalidHost");
|
||||
oHost = createObject("component","bugLog.components.host").init( oDF.getDAO("host") );
|
||||
oHost.setHostName(key);
|
||||
oHost.save();
|
||||
}
|
||||
|
||||
// store entry in cache
|
||||
variables.oHostCache.store( key, oHost );
|
||||
}
|
||||
</cfscript>
|
||||
<cfreturn oHost>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getSeverityFromBean" access="private" returntype="severity" hint="Uses the information on the rawEntryBean to retrieve the corresponding Severity object">
|
||||
<cfargument name="entryBean" type="rawEntryBean" required="true">
|
||||
<cfargument name="createIfNeeded" type="boolean" default="false">
|
||||
<cfscript>
|
||||
var key = "";
|
||||
var bean = arguments.entryBean;
|
||||
var oSeverity = 0;
|
||||
var oDF = variables.oDAOFactory;
|
||||
|
||||
key = bean.getSeverityCode();
|
||||
|
||||
try {
|
||||
// first we try to get it from the cache
|
||||
oSeverity = variables.oSeverityCache.retrieve( key );
|
||||
|
||||
} catch(cacheService.itemNotFound e) {
|
||||
// entry not in cache, so we get it from DB
|
||||
try {
|
||||
oSeverity = variables.oSeverityFinder.findByCode( key );
|
||||
|
||||
} catch(severityFinderException.codeNotFound e) {
|
||||
// code does not exist, so we need to create it (if autocreate enabled)
|
||||
if(!arguments.createIfNeeded) throw(message="Invalid Severity",type="invalidSeverity");
|
||||
oSeverity = createObject("component","bugLog.components.severity").init( oDF.getDAO("severity") );
|
||||
oSeverity.setCode( key );
|
||||
oSeverity.setName( key );
|
||||
oSeverity.save();
|
||||
}
|
||||
|
||||
// store entry in cache
|
||||
variables.oSeverityCache.store( key, oSeverity );
|
||||
}
|
||||
</cfscript>
|
||||
<cfreturn oSeverity>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getUserByAPIKey" access="private" returntype="user" hint="Finds a user object using by its API Key">
|
||||
<cfargument name="apiKey" type="string" required="true">
|
||||
<cfscript>
|
||||
var oUser = 0;
|
||||
|
||||
try {
|
||||
// first we try to get it from the cache
|
||||
oUser = variables.oUserCache.retrieve( apiKey );
|
||||
|
||||
} catch(cacheService.itemNotFound e) {
|
||||
// entry not in cache, so we get it from DB
|
||||
try {
|
||||
oUser = variables.oUserFinder.findByAPIKey( apiKey );
|
||||
|
||||
var qryUserApps = oDAOFactory.getDAO("userApplication").search(userID = oUser.getUserID());
|
||||
var apps = oAppFinder.findByIDList(valueList(qryUserApps.applicationID));
|
||||
oUser.setAllowedApplications(apps);
|
||||
|
||||
} catch(sourceFinderException.usernameNotFound e) {
|
||||
// code does not exist, so we need to create it (if autocreate enabled)
|
||||
throw(message="Invalid API Key",type="bugLog.invalidAPIKey");
|
||||
}
|
||||
|
||||
// store entry in cache
|
||||
variables.oUserCache.store( key, oUser );
|
||||
}
|
||||
|
||||
return oUser;
|
||||
</cfscript>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getSourceFromBean" access="private" returntype="source" hint="Uses the information on the rawEntryBean to retrieve the corresponding Source object">
|
||||
<cfargument name="entryBean" type="rawEntryBean" required="true">
|
||||
<cfargument name="createIfNeeded" type="boolean" default="false">
|
||||
<cfscript>
|
||||
var key = "";
|
||||
var bean = arguments.entryBean;
|
||||
var oSource = 0;
|
||||
var oDF = variables.oDAOFactory;
|
||||
|
||||
key = bean.getSourceName();
|
||||
|
||||
try {
|
||||
// first we try to get it from the cache
|
||||
oSource = variables.oSourceCache.retrieve( key );
|
||||
|
||||
} catch(cacheService.itemNotFound e) {
|
||||
// entry not in cache, so we get it from DB
|
||||
try {
|
||||
oSource = variables.oSourceFinder.findByName( key );
|
||||
|
||||
} catch(sourceFinderException.codeNotFound e) {
|
||||
// code does not exist, so we need to create it (if autocreate enabled)
|
||||
if(!arguments.createIfNeeded) throw(message="Invalid Source",type="invalidSource");
|
||||
oSource = createObject("component","bugLog.components.source").init( oDF.getDAO("source") );
|
||||
oSource.setName( key );
|
||||
oSource.save();
|
||||
}
|
||||
|
||||
// store entry in cache
|
||||
variables.oSourceCache.store( key, oSource );
|
||||
}
|
||||
</cfscript>
|
||||
<cfreturn oSource>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="loadRules" access="private" returntype="void" hint="this method loads the rules into the rule processor">
|
||||
<cfscript>
|
||||
var oRule = 0;
|
||||
var oExtensionsService = 0;
|
||||
var aRules = arrayNew(1);
|
||||
var i = 0;
|
||||
var dao = 0;
|
||||
var thisRule = 0;
|
||||
|
||||
// clear all existing rules
|
||||
variables.oRuleProcessor.flushRules();
|
||||
|
||||
// get the rule definitions from the extensions service
|
||||
dao = variables.oDAOFactory.getDAO("extension");
|
||||
oExtensionsService = createObject("component","bugLog.components.extensionsService").init( dao );
|
||||
aRules = oExtensionsService.getRules();
|
||||
|
||||
// create rule objects and load them into the rule processor
|
||||
for(i=1; i lte arrayLen(aRules);i=i+1) {
|
||||
thisRule = aRules[i];
|
||||
|
||||
if(thisRule.enabled) {
|
||||
oRule =
|
||||
thisRule.instance
|
||||
.setListener(this)
|
||||
.setDAOFactory( variables.oDAOFactory )
|
||||
.setMailerService( variables.mailerService );
|
||||
|
||||
// add rule to processor
|
||||
variables.oRuleProcessor.addRule(oRule);
|
||||
}
|
||||
}
|
||||
</cfscript>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="configureHistoryPurging" access="private" output="false" returntype="void">
|
||||
<cfset var enabled = getConfig().getSetting("purging.enabled")>
|
||||
<cfif enabled>
|
||||
<cfset scheduler.setupTask("bugLogPurgeHistory",
|
||||
"util/purgeHistory.cfm",
|
||||
"03:00",
|
||||
"daily") />
|
||||
<cfelse>
|
||||
<cfset scheduler.removeTask("bugLogPurgeHistory") />
|
||||
</cfif>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="configureDigest" access="private" output="false" returntype="void">
|
||||
<cfset var config = getConfig()>
|
||||
<cfset var enabled = config.getSetting("digest.enabled")>
|
||||
<cfset var interval = config.getSetting("digest.schedulerIntervalHours")>
|
||||
<cfset var startTime = config.getSetting("digest.schedulerStartTime")>
|
||||
|
||||
<cfif enabled>
|
||||
<cfset scheduler.setupTask("bugLogSendDigest",
|
||||
"util/sendDigest.cfm",
|
||||
startTime,
|
||||
interval*3600) />
|
||||
<cfelse>
|
||||
<cfset scheduler.removeTask("bugLogSendDigest") />
|
||||
</cfif>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="allowAutoCreate" returnType="boolean" access="private">
|
||||
<cfargument name="entityType" type="string" required="true">
|
||||
<cfreturn getConfig().getSetting("autocreate." & arguments.entityType, variables.autoCreateDefault)>
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,874 +0,0 @@
|
||||
component {
|
||||
variables._fw1_version = "3.5.0";
|
||||
variables._di1_version = "1.1.1";
|
||||
/*
|
||||
Copyright (c) 2010-2015, Sean Corfield
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// CONSTRUCTOR
|
||||
|
||||
public any function init( any folders, struct config = { } ) {
|
||||
variables.folderList = folders;
|
||||
variables.folderArray = folders;
|
||||
if ( isSimpleValue( folders ) ) {
|
||||
variables.folderArray = listToArray( folders );
|
||||
} else {
|
||||
variables.folderList = arrayToList( folders );
|
||||
}
|
||||
var n = arrayLen( variables.folderArray );
|
||||
for ( var i = 1; i <= n; ++i ) {
|
||||
variables.folderArray[ i ] = trim( variables.folderArray[ i ] );
|
||||
}
|
||||
variables.config = config;
|
||||
variables.beanInfo = { };
|
||||
variables.beanCache = { };
|
||||
variables.resolutionCache = { };
|
||||
variables.initMethodCache = { };
|
||||
variables.settersInfo = { };
|
||||
variables.autoExclude = [
|
||||
'/WEB-INF', '/Application.cfc', // never manage these!
|
||||
'/Application.lc', '/Application.lucee',
|
||||
// assume default name for intermediary:
|
||||
'/MyApplication.cfc',
|
||||
'/MyApplication.lc', '/MyApplication.lucee',
|
||||
'framework.cfc', 'ioc.cfc', // legacy FW/1 / DI/1
|
||||
// recent FW/1 + DI/1 + AOP/1 exclusions:
|
||||
'/framework/aop.cfc', '/framework/beanProxy.cfc',
|
||||
'/framework/ioc.cfc', '/framework/WireBoxAdapter.cfc',
|
||||
'/framework/one.cfc',
|
||||
// and Clojure-related exclusions:
|
||||
'/framework/cfmljure.cfc', '/framework/cljcontroller.cfc',
|
||||
'/framework/ioclj.cfc'
|
||||
];
|
||||
variables.listeners = 0;
|
||||
setupFrameworkDefaults();
|
||||
if ( structKeyExists( variables.config, 'loadListener' ) ) {
|
||||
this.onLoad( variables.config.loadListener );
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// PUBLIC METHODS
|
||||
|
||||
// programmatically register an alias
|
||||
public any function addAlias( string aliasName, string beanName ) {
|
||||
discoverBeans();
|
||||
variables.beanInfo[ aliasName ] = variables.beanInfo[ beanName ];
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// programmatically register new beans with the factory (add a singleton name/value pair)
|
||||
public any function addBean( string beanName, any beanValue ) {
|
||||
discoverBeans();
|
||||
variables.beanInfo[ beanName ] = {
|
||||
name = beanName, value = beanValue, isSingleton = true
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// return true if the factory (or a parent factory) knows about the requested bean
|
||||
public boolean function containsBean( string beanName ) {
|
||||
discoverBeans();
|
||||
return structKeyExists( variables.beanInfo, beanName ) ||
|
||||
( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) );
|
||||
}
|
||||
|
||||
|
||||
// programmatically register new beans with the factory (add an actual CFC)
|
||||
public any function declareBean( string beanName, string dottedPath, boolean isSingleton = true, struct overrides = { } ) {
|
||||
discoverBeans();
|
||||
var singleDir = '';
|
||||
if ( listLen( dottedPath, '.' ) > 1 ) {
|
||||
var cfc = listLast( dottedPath, '.' );
|
||||
var dottedPart = left( dottedPath, len( dottedPath ) - len( cfc ) - 1 );
|
||||
singleDir = singular( listLast( dottedPart, '.' ) );
|
||||
}
|
||||
var basePath = replace( dottedPath, '.', '/', 'all' );
|
||||
var cfcPath = expandPath( '/' & basePath & '.cfc' );
|
||||
var expPath = cfcPath;
|
||||
if ( !fileExists( expPath ) ) expPath = expandPath( '/' & basePath & '.lc' );
|
||||
if ( !fileExists( expPath ) ) expPath = expandPath( '/' & basePath & '.lucee' );
|
||||
if ( !fileExists( expPath ) ) throw "Unable to find source file for #dottedPath#: expands to #cfcPath#";
|
||||
var cfcPath = replace( expPath, chr(92), '/', 'all' );
|
||||
var metadata = {
|
||||
name = beanName, qualifier = singleDir, isSingleton = isSingleton,
|
||||
path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath ),
|
||||
overrides = overrides
|
||||
};
|
||||
variables.beanInfo[ beanName ] = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
public any function factoryBean( string beanName, any factory, string methodName, array args = [ ], struct overrides = { } ) {
|
||||
discoverBeans();
|
||||
var metadata = {
|
||||
name = beanName, isSingleton = false, // really?
|
||||
factory = factory, method = methodName, args = args,
|
||||
overrides = overrides
|
||||
};
|
||||
variables.beanInfo[ beanName ] = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// return the requested bean, fully populated
|
||||
public any function getBean( string beanName ) {
|
||||
discoverBeans();
|
||||
if ( structKeyExists( variables.beanInfo, beanName ) ) {
|
||||
return resolveBean( beanName );
|
||||
} else if ( structKeyExists( variables, 'parent' ) ) {
|
||||
return variables.parent.getBean( beanName );
|
||||
} else {
|
||||
throw 'bean not found: #beanName#';
|
||||
}
|
||||
}
|
||||
|
||||
// convenience API for metaprogramming perhaps?
|
||||
public any function getBeanInfo( string beanName = '', boolean flatten = false,
|
||||
string regex = '' ) {
|
||||
discoverBeans();
|
||||
if ( len( beanName ) ) {
|
||||
// ask about a specific bean:
|
||||
if ( structKeyExists( variables.beanInfo, beanName ) ) {
|
||||
return variables.beanInfo[ beanName ];
|
||||
}
|
||||
if ( structKeyExists( variables, 'parent' ) ) {
|
||||
return parentBeanInfo( beanName );
|
||||
}
|
||||
throw 'bean not found: #beanName#';
|
||||
} else {
|
||||
var result = { beanInfo = { } };
|
||||
if ( structKeyExists( variables, 'parent' ) ) {
|
||||
if ( flatten || len( regex ) ) {
|
||||
structAppend( result.beanInfo, parentBeanInfoList( flatten ).beanInfo );
|
||||
structAppend( result.beanInfo, variables.beanInfo );
|
||||
} else {
|
||||
result.beanInfo = variables.beanInfo;
|
||||
result.parent = parentBeanInfoList( flatten );
|
||||
};
|
||||
} else {
|
||||
result.beanInfo = variables.beanInfo;
|
||||
}
|
||||
if ( len( regex ) ) {
|
||||
var matched = { };
|
||||
for ( var name in result.beanInfo ) {
|
||||
if ( REFind( regex, name ) ) {
|
||||
matched[ name ] = result.beanInfo[ name ];
|
||||
}
|
||||
}
|
||||
result.beanInfo = matched;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// return a copy of the DI/1 configuration
|
||||
public struct function getConfig() {
|
||||
// note: we only make a shallow copy
|
||||
return structCopy( variables.config );
|
||||
}
|
||||
|
||||
|
||||
// return the DI/1 version
|
||||
public string function getVersion() {
|
||||
return variables.config.version;
|
||||
}
|
||||
|
||||
|
||||
// return true iff bean is known to be a singleton
|
||||
public boolean function isSingleton( string beanName ) {
|
||||
discoverBeans();
|
||||
if ( structKeyExists( variables.beanInfo, beanName ) ) {
|
||||
return variables.beanInfo[ beanName ].isSingleton;
|
||||
} else if ( structKeyExists( variables, 'parent' ) ) {
|
||||
try {
|
||||
return variables.parent.isSingleton( beanName );
|
||||
} catch ( any e ) {
|
||||
return false; // parent doesn't know the bean therefore is it not singleton
|
||||
}
|
||||
} else {
|
||||
return false; // we don't know the bean therefore it is not a managed singleton
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// given a bean (by name, by type or by value), call the named
|
||||
// setters with the specified property values
|
||||
public any function injectProperties( any bean, struct properties ) {
|
||||
if ( isSimpleValue( bean ) ) {
|
||||
if ( containsBean( bean ) ) bean = getBean( bean );
|
||||
else bean = construct( bean );
|
||||
}
|
||||
for ( var property in properties ) {
|
||||
if ( !isNull( properties[ property ] ) ) {
|
||||
var args = { };
|
||||
args[ property ] = properties[ property ];
|
||||
evaluate( 'bean.set#property#( argumentCollection = args )' );
|
||||
}
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
|
||||
// empty the cache and reload all the singleton beans
|
||||
// note: this does not reload the parent - if you have parent/child factories you
|
||||
// are responsible for dealing with that logic (it's safe to reload a child but
|
||||
// if you reload the parent, you must reload *all* child factories to ensure
|
||||
// things stay consistent!)
|
||||
public any function load() {
|
||||
discoverBeans();
|
||||
variables.beanCache = { };
|
||||
variables.resolutionCache = { };
|
||||
variables.initMethodCache = { };
|
||||
for ( var key in variables.beanInfo ) {
|
||||
if ( !structKeyExists( variables.beanInfo[ key ], "isSingleton" ) )
|
||||
throw "internal error: bean #key# has no isSingleton flag!";
|
||||
if ( variables.beanInfo[ key ].isSingleton ) {
|
||||
getBean( key );
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// add a listener for processing after a (re)load of the factory
|
||||
// called with just the factory, should be a plain function
|
||||
public any function onLoad( any listener ) {
|
||||
var head = { next = variables.listeners, listener = listener };
|
||||
variables.listeners = head;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// set the parent bean factory
|
||||
public any function setParent( any parent ) {
|
||||
variables.parent = parent;
|
||||
return this;
|
||||
}
|
||||
|
||||
// PRIVATE METHODS
|
||||
|
||||
private boolean function beanIsTransient( string singleDir, string dir, string beanName ) {
|
||||
return singleDir == 'bean' ||
|
||||
structKeyExists( variables.transients, dir ) ||
|
||||
( structKeyExists( variables.config, "singletonPattern" ) &&
|
||||
refindNoCase( variables.config.singletonPattern, beanName ) == 0 ) ||
|
||||
( structKeyExists( variables.config, "transientPattern" ) &&
|
||||
refindNoCase( variables.config.transientPattern, beanName ) > 0 );
|
||||
}
|
||||
|
||||
|
||||
private any function cachable( string beanName) {
|
||||
var newObject = false;
|
||||
var info = variables.beanInfo[ beanName ];
|
||||
if ( info.isSingleton ) {
|
||||
// cache on the qualified bean name:
|
||||
var qualifiedName = beanName;
|
||||
if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) {
|
||||
qualifiedName = info.name & info.qualifier;
|
||||
}
|
||||
if ( !structKeyExists( variables.beanCache, qualifiedName ) ) {
|
||||
variables.beanCache[ qualifiedName ] = construct( info.cfc );
|
||||
newObject = true;
|
||||
}
|
||||
return { bean = variables.beanCache[ qualifiedName ], newObject = newObject };
|
||||
} else {
|
||||
return { bean = construct( info.cfc ), newObject = true };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct function cleanMetadata( string cfc ) {
|
||||
var baseMetadata = metadata( cfc );
|
||||
var iocMeta = { setters = { }, pruned = false, type = baseMetadata.type };
|
||||
var md = { extends = baseMetadata };
|
||||
do {
|
||||
md = md.extends;
|
||||
// gather up setters based on metadata:
|
||||
var implicitSetters = false;
|
||||
// we have implicit setters if: accessors="true" or persistent="true"
|
||||
if ( structKeyExists( md, 'persistent' ) && isBoolean( md.persistent ) ) {
|
||||
implicitSetters = md.persistent;
|
||||
}
|
||||
if ( structKeyExists( md, 'accessors' ) && isBoolean( md.accessors ) ) {
|
||||
implicitSetters = implicitSetters || md.accessors;
|
||||
}
|
||||
if ( structKeyExists( md, 'properties' ) ) {
|
||||
// due to a bug in ACF9.0.1, we cannot use var property in md.properties,
|
||||
// instead we must use an explicit loop index... ugh!
|
||||
var n = arrayLen( md.properties );
|
||||
for ( var i = 1; i <= n; ++i ) {
|
||||
var property = md.properties[ i ];
|
||||
if ( implicitSetters &&
|
||||
( !structKeyExists( property, 'setter' ) ||
|
||||
isBoolean( property.setter ) && property.setter ) ) {
|
||||
if ( structKeyExists( property, 'type' ) &&
|
||||
property.type != 'any' ) {
|
||||
iocMeta.setters[ property.name ] = 'typed';
|
||||
} else {
|
||||
iocMeta.setters[ property.name ] = 'implicit';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// still looking for a constructor?
|
||||
if ( !structKeyExists( iocMeta, 'constructor' ) ) {
|
||||
if ( structKeyExists( md, 'functions' ) ) {
|
||||
// due to a bug in ACF9.0.1, we cannot use var property in md.functions,
|
||||
// instead we must use an explicit loop index... ugh!
|
||||
var n = arrayLen( md.functions );
|
||||
for ( var i = 1; i <= n; ++i ) {
|
||||
var func = md.functions[ i ];
|
||||
if ( func.name == 'init' ) {
|
||||
iocMeta.constructor = { };
|
||||
if ( structKeyExists( func, 'parameters' ) ) {
|
||||
// due to a bug in ACF9.0.1, we cannot use var arg in func.parameters,
|
||||
// instead we must use an explicit loop index... ugh!
|
||||
var m = arrayLen( func.parameters );
|
||||
for ( var j = 1; j <= m; ++j ) {
|
||||
var arg = func.parameters[ j ];
|
||||
iocMeta.constructor[ arg.name ] = structKeyExists( arg, 'required' ) ? arg.required : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while ( structKeyExists( md, 'extends' ) );
|
||||
return iocMeta;
|
||||
}
|
||||
|
||||
|
||||
// in case an extension point wants to override actual construction:
|
||||
private any function construct( string dottedPath ) {
|
||||
return createObject( 'component', dottedPath );
|
||||
}
|
||||
|
||||
|
||||
// in case an extension point wants to override actual metadata retrieval:
|
||||
private any function metadata( string dottedPath ) {
|
||||
try {
|
||||
return getComponentMetadata( dottedPath );
|
||||
} catch ( any e ) {
|
||||
var except = "Unable to getComponentMetadata(#dottedPath#) because: " &
|
||||
e.message & ( len( e.detail ) ? " (#e.detail#)" : "" );
|
||||
throw except;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private string function deduceDottedPath( string baseMapping, string basePath ) {
|
||||
if ( right( basePath, 1 ) == '/' && len( basePath ) > 1 ) {
|
||||
basePath = left( basePath, len( basePath ) - 1 );
|
||||
}
|
||||
var cfcPath = left( baseMapping, 1 ) == '/' ?
|
||||
( len( baseMapping ) > 1 ? right( baseMapping, len( baseMapping ) - 1 ) : '' ) :
|
||||
getFileFromPath( baseMapping );
|
||||
if ( right( cfcPath, 1 ) == '/' && len( cfcPath ) > 1 ) {
|
||||
cfcPath = left( cfcPath, len( cfcPath ) - 1 );
|
||||
}
|
||||
var expPath = basePath;
|
||||
var notFound = true;
|
||||
var dotted = '';
|
||||
do {
|
||||
var mapped = cfcPath;
|
||||
if ( len( mapped ) && left( mapped, 1 ) != '.' ) mapped = '/' & mapped;
|
||||
var mappedPath = replace( expandpath( mapped ), chr(92), '/', 'all' );
|
||||
if ( mappedPath == basePath ) {
|
||||
dotted = replace( cfcPath, '/', '.', 'all' );
|
||||
notFound = false;
|
||||
break;
|
||||
}
|
||||
var prevPath = expPath;
|
||||
expPath = replace( getDirectoryFromPath( expPath ), chr(92), '/', 'all' );
|
||||
if ( right( expPath, 1 ) == '/' && len( expPath ) > 1 ) {
|
||||
expPath = left( expPath, len( expPath ) - 1 );
|
||||
}
|
||||
var progress = prevPath != expPath;
|
||||
var piece = listLast( expPath, '/' );
|
||||
cfcPath = piece & '/' & cfcPath;
|
||||
} while ( progress );
|
||||
if ( notFound ) {
|
||||
throw 'unable to deduce dot-relative path for: #baseMapping# (#basePath#) root #expandPath("/")#';
|
||||
}
|
||||
return dotted;
|
||||
}
|
||||
|
||||
|
||||
private void function discoverBeans() {
|
||||
if ( structKeyExists( variables, 'discoveryComplete' ) ) return;
|
||||
lock name="#application.applicationName#_ioc1_#variables.folderList#" type="exclusive" timeout="30" {
|
||||
if ( structKeyExists( variables, 'discoveryComplete' ) ) return;
|
||||
variables.pathMapCache = { };
|
||||
for ( var f in variables.folderArray ) {
|
||||
discoverBeansInFolder( replace( f, chr(92), '/', 'all' ) );
|
||||
}
|
||||
variables.discoveryComplete = true;
|
||||
}
|
||||
onLoadEvent();
|
||||
}
|
||||
|
||||
|
||||
private void function discoverBeansInFolder( string mapping ) {
|
||||
var folder = replace( expandPath( mapping ), chr(92), '/', 'all' );
|
||||
var dotted = deduceDottedPath( mapping, folder );
|
||||
var cfcs = [ ];
|
||||
try {
|
||||
cfcs = directoryList( folder, variables.config.recurse, 'path', '*.cfc' );
|
||||
var lcs = directoryList( folder, variables.config.recurse, 'path', '*.lc' );
|
||||
for ( var l in lcs ) arrayAppend( cfcs, l );
|
||||
lcs = directoryList( folder, variables.config.recurse, 'path', '*.lucee' );
|
||||
for ( l in lcs ) arrayAppend( cfcs, l );
|
||||
} catch ( any e ) {
|
||||
// assume bad path - ignore it, cfcs is empty list
|
||||
}
|
||||
for ( var cfcOSPath in cfcs ) {
|
||||
var cfcPath = replace( cfcOSPath, chr(92), '/', 'all' );
|
||||
// watch out for excluded paths:
|
||||
var excludePath = false;
|
||||
for ( var pattern in variables.config.exclude ) {
|
||||
if ( findNoCase( pattern, cfcPath ) ) {
|
||||
excludePath = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( excludePath ) continue;
|
||||
var relPath = right( cfcPath, len( cfcPath ) - len( folder ) );
|
||||
var extN = 1 + len( listLast( cfcPath, "." ) );
|
||||
relPath = left( relPath, len( relPath ) - extN );
|
||||
var dir = listLast( getDirectoryFromPath( cfcPath ), '/' );
|
||||
var singleDir = singular( dir );
|
||||
var beanName = listLast( relPath, '/' );
|
||||
var dottedPath = dotted & replace( relPath, '/', '.', 'all' );
|
||||
try {
|
||||
var metadata = {
|
||||
name = beanName, qualifier = singleDir, isSingleton = !beanIsTransient( singleDir, dir, beanName ),
|
||||
path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath )
|
||||
};
|
||||
if ( structKeyExists( metadata.metadata, "type" ) && metadata.metadata.type == "interface" ) {
|
||||
continue;
|
||||
}
|
||||
if ( structKeyExists( variables.beanInfo, beanName ) ) {
|
||||
if ( variables.config.omitDirectoryAliases ) {
|
||||
throw '#beanName# is not unique (and omitDirectoryAliases is true)';
|
||||
}
|
||||
structDelete( variables.beanInfo, beanName );
|
||||
variables.beanInfo[ beanName & singleDir ] = metadata;
|
||||
} else {
|
||||
variables.beanInfo[ beanName ] = metadata;
|
||||
if ( !variables.config.omitDirectoryAliases ) {
|
||||
variables.beanInfo[ beanName & singleDir ] = metadata;
|
||||
}
|
||||
}
|
||||
} catch ( any e ) {
|
||||
// wrap the exception so we can add bean name for debugging
|
||||
// this trades off any stack trace information for the bean name but
|
||||
// since we are only trying to get metadata, the latter should be
|
||||
// more useful than the former
|
||||
var except = "Problem with metadata for #beanName# (#dottedPath#) because: " &
|
||||
e.message & ( len( e.detail ) ? " (#e.detail#)" : "" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct function findSetters( any cfc, struct iocMeta ) {
|
||||
var liveMeta = { setters = iocMeta.setters };
|
||||
if ( !iocMeta.pruned ) {
|
||||
// need to prune known setters of transients:
|
||||
var prunable = { };
|
||||
for ( var known in iocMeta.setters ) {
|
||||
if ( !isSingleton( known ) ) {
|
||||
prunable[ known ] = true;
|
||||
}
|
||||
}
|
||||
for ( known in prunable ) {
|
||||
structDelete( iocMeta.setters, known );
|
||||
}
|
||||
iocMeta.pruned = true;
|
||||
}
|
||||
// gather up explicit setters:
|
||||
for ( var member in cfc ) {
|
||||
var method = cfc[ member ];
|
||||
var n = len( member );
|
||||
if ( isCustomFunction( method ) && left( member, 3 ) == 'set' && n > 3 ) {
|
||||
var property = right( member, n - 3 );
|
||||
if ( !isSingleton( property ) ) {
|
||||
// ignore properties that we know to be transients...
|
||||
continue;
|
||||
}
|
||||
if ( !structKeyExists( liveMeta.setters, property ) ) {
|
||||
liveMeta.setters[ property ] = 'explicit';
|
||||
}
|
||||
}
|
||||
}
|
||||
return liveMeta;
|
||||
}
|
||||
|
||||
|
||||
private any function forceCache( any bean, string beanName) {
|
||||
var info = variables.beanInfo[ beanName ];
|
||||
if ( info.isSingleton ) {
|
||||
// cache on the qualified bean name:
|
||||
var qualifiedName = beanName;
|
||||
if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) {
|
||||
qualifiedName = info.name & info.qualifier;
|
||||
}
|
||||
variables.beanCache[ qualifiedName ] = bean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean function isConstant ( string beanName ) {
|
||||
return structKeyExists( variables.beanInfo, beanName ) &&
|
||||
structKeyExists( variables.beanInfo[ beanName ], 'value' );
|
||||
}
|
||||
|
||||
|
||||
private void function logMissingBean( string beanName, string resolvingBeanName = '' ) {
|
||||
var sys = createObject( 'java', 'java.lang.System' );
|
||||
if ( len( resolvingBeanName ) ) {
|
||||
sys.out.println( 'bean not found: #beanName#; while resolving #resolvingBeanName#' );
|
||||
} else {
|
||||
sys.out.println( 'bean not found: #beanName#' );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void function missingBean( string beanName, string resolvingBeanName = '' ) {
|
||||
if ( variables.config.strict ) {
|
||||
if ( len( resolvingBeanName ) ) {
|
||||
throw 'bean not found: #beanName#; while resolving #resolvingBeanName#';
|
||||
} else {
|
||||
throw 'bean not found: #beanName#';
|
||||
}
|
||||
} else {
|
||||
logMissingBean( beanName, resolvingBeanName );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void function onLoadEvent() {
|
||||
var head = variables.listeners;
|
||||
while ( isStruct( head ) ) {
|
||||
if ( isCustomFunction( head.listener ) ||
|
||||
( listFirst( server.coldfusion.productVersion ) >= 10 &&
|
||||
isClosure( head.listener ) ) ) {
|
||||
head.listener( this );
|
||||
} else if ( isObject( head.listener ) ) {
|
||||
head.listener.onLoad( this );
|
||||
} else if ( isSimpleValue( head.listener ) &&
|
||||
containsBean( head.listener ) ) {
|
||||
getBean( head.listener ).onLoad( this );
|
||||
} else {
|
||||
throw "invalid onLoad listener registered: #head.listener.toString()#";
|
||||
}
|
||||
head = head.next;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private any function parentBeanInfo( string beanName ) {
|
||||
// intended to be adaptable to whatever the parent is:
|
||||
if ( structKeyExists( variables.parent, 'getBeanInfo' ) ) {
|
||||
// smells like DI/1 or compatible:
|
||||
return variables.parent.getBeanInfo( beanName );
|
||||
}
|
||||
if ( structKeyExists( variables.parent, 'getBeanDefinition' ) ) {
|
||||
// smells like ColdSpring or compatible:
|
||||
return variables.parent.getBeanDefinition( beanName );
|
||||
}
|
||||
// unknown:
|
||||
return { };
|
||||
}
|
||||
|
||||
|
||||
private any function parentBeanInfoList( boolean flatten ) {
|
||||
// intended to be adaptable to whatever the parent is:
|
||||
if ( structKeyExists( variables.parent, 'getBeanInfo' ) ) {
|
||||
// smells like DI/1 or compatible:
|
||||
return variables.parent.getBeanInfo( flatten = flatten );
|
||||
}
|
||||
if ( structKeyExists( variables.parent, 'getBeanDefinitionList' ) ) {
|
||||
// smells like ColdSpring or compatible:
|
||||
return variables.parent.getBeanDefinitionList();
|
||||
}
|
||||
// unknown
|
||||
return { };
|
||||
}
|
||||
|
||||
|
||||
private any function resolveBean( string beanName ) {
|
||||
// do enough resolution to create and initialization this bean
|
||||
// returns a struct of the bean and a struct of beans and setters still to run
|
||||
// construction phase:
|
||||
var partialBean = resolveBeanCreate( beanName, { injection = { }, dependencies = { } } );
|
||||
if ( structKeyExists( variables.resolutionCache, beanName ) &&
|
||||
variables.resolutionCache[ beanName ] ) {
|
||||
// fully resolved, no action needed this time
|
||||
} else {
|
||||
var checkForPostInjection = structKeyExists( variables.config, 'initMethod' );
|
||||
var initMethod = checkForPostInjection ? variables.config.initMethod : '';
|
||||
var postInjectables = { };
|
||||
// injection phase:
|
||||
// now perform all of the injection:
|
||||
for ( var name in partialBean.injection ) {
|
||||
var injection = partialBean.injection[ name ];
|
||||
if ( checkForPostInjection && !isConstant( name ) && structKeyExists( injection.bean, initMethod ) ) {
|
||||
postInjectables[ name ] = true;
|
||||
}
|
||||
for ( var property in injection.setters ) {
|
||||
if ( injection.setters[ property ] == 'typed' &&
|
||||
variables.config.omitTypedProperties ) {
|
||||
// we do not inject typed properties!
|
||||
continue;
|
||||
}
|
||||
var args = { };
|
||||
if ( structKeyExists( injection.overrides, property ) ) {
|
||||
args[ property ] = injection.overrides[ property ];
|
||||
} else if ( structKeyExists( partialBean.injection, property ) ) {
|
||||
args[ property ] = partialBean.injection[ property ].bean;
|
||||
} else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( property ) ) {
|
||||
args[ property ] = variables.parent.getBean( property );
|
||||
} else {
|
||||
missingBean( property, beanName );
|
||||
continue;
|
||||
}
|
||||
evaluate( 'injection.bean.set#property#( argumentCollection = args )' );
|
||||
}
|
||||
}
|
||||
// post-injection, pre-init-method phase:
|
||||
for ( name in partialBean.injection ) {
|
||||
injection = partialBean.injection[ name ];
|
||||
setupInitMethod( name, injection.bean );
|
||||
}
|
||||
// see if anything needs post-injection, init-method calls:
|
||||
for ( var postName in postInjectables ) {
|
||||
callInitMethod( postName, postInjectables, partialBean, initMethod );
|
||||
}
|
||||
variables.resolutionCache[ beanName ] = isSingleton( beanName );
|
||||
}
|
||||
return partialBean.bean;
|
||||
}
|
||||
|
||||
|
||||
private void function callInitMethod( string name, struct injectables, struct info, string method ) {
|
||||
|
||||
if ( injectables[ name ] ) {
|
||||
injectables[ name ] = false; // this ensures we don't try to init the same
|
||||
// bean twice - and also breaks circular dependencies...
|
||||
if ( structKeyExists( info.dependencies, name ) ) {
|
||||
for ( var depName in info.dependencies[ name ] ) {
|
||||
if ( structKeyExists( injectables, depName ) &&
|
||||
injectables[ depName ] ) {
|
||||
callInitMethod( depName, injectables, info, method );
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( structKeyExists( variables.initMethodCache, name ) &&
|
||||
variables.initMethodCache[ name ] ) {
|
||||
} else {
|
||||
variables.initMethodCache[ name ] = isSingleton( name );
|
||||
var bean = info.injection[ name ].bean;
|
||||
evaluate( 'bean.#method#()' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct function resolveBeanCreate( string beanName, struct accumulator ) {
|
||||
var bean = 0;
|
||||
if ( structKeyExists( variables.beanInfo, beanName ) ) {
|
||||
var info = variables.beanInfo[ beanName ];
|
||||
accumulator.dependencies[ beanName ] = { };
|
||||
if ( structKeyExists( info, 'cfc' ) ) {
|
||||
/*******************************************************/
|
||||
var metaBean = cachable( beanName );
|
||||
var overrides = structKeyExists( info, 'overrides' ) ? info.overrides : { };
|
||||
bean = metaBean.bean;
|
||||
if ( metaBean.newObject ) {
|
||||
if ( structKeyExists( info.metadata, 'constructor' ) ) {
|
||||
var args = { };
|
||||
for ( var arg in info.metadata.constructor ) {
|
||||
accumulator.dependencies[ beanName ][ arg ] = true;
|
||||
var argBean = { };
|
||||
// handle known required arguments
|
||||
if ( info.metadata.constructor[ arg ] ) {
|
||||
var beanMissing = true;
|
||||
if ( structKeyExists( overrides, arg ) ) {
|
||||
args[ arg ] = overrides[ arg ];
|
||||
beanMissing = false;
|
||||
} else if ( containsBean( arg ) ) {
|
||||
argBean = resolveBeanCreate( arg, accumulator );
|
||||
if ( structKeyExists( argBean, 'bean' ) ) {
|
||||
args[ arg ] = argBean.bean;
|
||||
beanMissing = false;
|
||||
}
|
||||
}
|
||||
if ( beanMissing ) {
|
||||
throw 'bean not found: #arg#; while resolving constructor arguments for #beanName#';
|
||||
}
|
||||
} else {
|
||||
if ( structKeyExists( overrides, arg ) ) {
|
||||
args[ arg ] = overrides[ arg ];
|
||||
} else if ( containsBean( arg ) ) {
|
||||
// optional but present
|
||||
argBean = resolveBeanCreate( arg, accumulator );
|
||||
if ( structKeyExists( argBean, 'bean' ) ) {
|
||||
args[ arg ] = argBean.bean;
|
||||
}
|
||||
} else {
|
||||
// optional but not present
|
||||
}
|
||||
}
|
||||
}
|
||||
var __ioc_newBean = evaluate( 'bean.init( argumentCollection = args )' );
|
||||
// if the constructor returns anything, it becomes the bean
|
||||
// this allows for smart constructors that return things other
|
||||
// than the CFC being created, such as implicit factory beans
|
||||
// and automatic singletons etc (rare practices in CFML but...)
|
||||
if ( !isNull( __ioc_newBean ) ) {
|
||||
bean = __ioc_newBean;
|
||||
forceCache( bean, beanName );
|
||||
}
|
||||
}
|
||||
}
|
||||
/*******************************************************/
|
||||
if ( !structKeyExists( accumulator.injection, beanName ) ) {
|
||||
if ( !structKeyExists( variables.settersInfo, beanName ) ) {
|
||||
variables.settersInfo[ beanName ] = findSetters( bean, info.metadata );
|
||||
}
|
||||
var setterMeta = {
|
||||
setters = variables.settersInfo[ beanName ].setters,
|
||||
bean = bean,
|
||||
overrides = overrides
|
||||
};
|
||||
accumulator.injection[ beanName ] = setterMeta;
|
||||
for ( var property in setterMeta.setters ) {
|
||||
accumulator.dependencies[ beanName ][ property ] = true;
|
||||
if ( structKeyExists( overrides, property ) ) {
|
||||
// skip resolution because we'll inject override
|
||||
} else {
|
||||
resolveBeanCreate( property, accumulator );
|
||||
}
|
||||
}
|
||||
}
|
||||
accumulator.bean = bean;
|
||||
} else if ( isConstant( beanName ) ) {
|
||||
accumulator.bean = info.value;
|
||||
accumulator.injection[ beanName ] = { bean = info.value, setters = { } };
|
||||
} else if ( structKeyExists( info, 'factory' ) ) {
|
||||
var fmBean = isSimpleValue( info.factory ) ? this.getBean( info.factory ) : info.factory;
|
||||
var nArgs = arrayLen( info.args );
|
||||
var argStruct = { };
|
||||
for ( var i = 1; i <= nArgs; ++i ) {
|
||||
var argName = info.args[ i ];
|
||||
if ( structKeyExists( info.overrides, argName ) ) {
|
||||
argStruct[ i ] = info.overrides[ argName ];
|
||||
} else {
|
||||
argStruct[ i ] = this.getBean( argName );
|
||||
}
|
||||
}
|
||||
accumulator.bean = evaluate( 'fmBean.#info.method#(argumentCollection=argStruct)' );
|
||||
accumulator.injection[ beanName ] = { bean = accumulator.bean, setters = { } };
|
||||
} else {
|
||||
throw 'internal error: invalid metadata for #beanName#';
|
||||
}
|
||||
} else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) ) {
|
||||
bean = variables.parent.getBean( beanName );
|
||||
accumulator.injection[ beanName ] = { bean = bean, setters = { } };
|
||||
accumulator.bean = bean;
|
||||
} else {
|
||||
missingBean( beanName );
|
||||
}
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
|
||||
private void function setupFrameworkDefaults() {
|
||||
param name = "variables.config.recurse" default = true;
|
||||
param name = "variables.config.strict" default = false;
|
||||
|
||||
if ( !structKeyExists( variables.config, 'exclude' ) ) {
|
||||
variables.config.exclude = [ ];
|
||||
}
|
||||
for ( var elem in variables.autoExclude ) {
|
||||
arrayAppend( variables.config.exclude, replace( elem, chr(92), '/', 'all' ) );
|
||||
}
|
||||
|
||||
// install bean factory constant:
|
||||
variables.beanInfo.beanFactory = { value = this, isSingleton = true };
|
||||
if ( structKeyExists( variables.config, 'constants' ) ) {
|
||||
for ( var beanName in variables.config.constants ) {
|
||||
variables.beanInfo[ beanName ] = { value = variables.config.constants[ beanName ], isSingleton = true };
|
||||
}
|
||||
}
|
||||
|
||||
variables.transients = { };
|
||||
if ( structKeyExists( variables.config, 'transients' ) ) {
|
||||
for ( var transientFolder in variables.config.transients ) {
|
||||
variables.transients[ transientFolder ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( structKeyExists( variables.config, 'singletonPattern' ) &&
|
||||
structKeyExists( variables.config, 'transientPattern' ) ) {
|
||||
throw 'singletonPattern and transientPattern are mutually exclusive';
|
||||
}
|
||||
|
||||
if ( !structKeyExists( variables.config, 'omitTypedProperties' ) ) {
|
||||
variables.config.omitTypedProperties = false;
|
||||
}
|
||||
if ( !structKeyExists( variables.config, 'omitDirectoryAliases' ) ) {
|
||||
variables.config.omitDirectoryAliases = false;
|
||||
}
|
||||
if ( !structKeyExists( variables.config, 'singulars' ) ) {
|
||||
variables.config.singulars = { };
|
||||
}
|
||||
if ( !structKeyExists( variables.config, 'liberal' ) ) {
|
||||
variables.config.liberal = false;
|
||||
}
|
||||
|
||||
variables.config.version = variables._di1_version;
|
||||
}
|
||||
|
||||
|
||||
// hook for extension points to process beans after they have been
|
||||
// constructed and injected, but before init-method is called on anything
|
||||
private void function setupInitMethod( string name, any bean ) {
|
||||
}
|
||||
|
||||
|
||||
private string function singular( string plural ) {
|
||||
if ( structKeyExists( variables.config.singulars, plural ) ) {
|
||||
return variables.config.singulars[ plural ];
|
||||
}
|
||||
var single = plural;
|
||||
var n = len( plural );
|
||||
var last = right( plural, 1 );
|
||||
if ( last == 's' ) {
|
||||
if ( variables.config.liberal && n > 3 && right( plural, 3 ) == 'ies' ) {
|
||||
single = left( plural, n - 3 ) & 'y';
|
||||
} else {
|
||||
single = left( plural, n - 1 );
|
||||
}
|
||||
}
|
||||
return single;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/basicauth">
|
||||
|
||||
<cffunction name="get">
|
||||
<cfargument name="username" default="" />
|
||||
<cfargument name="password" default="" />
|
||||
<cfreturn representationOf(arguments) />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,11 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/conflict/{URI}, /conflict/{URI}" hint="I have multiple uris">
|
||||
|
||||
<cffunction name="get">
|
||||
<cfargument name="uri" type="string" default="0" />
|
||||
<cfset local.res = {} />
|
||||
<cfset local.res.uri = arguments.uri />
|
||||
|
||||
<cfreturn representationOf(local.res) />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,27 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo/{id}" hint="I am the component hint.">
|
||||
|
||||
<cfproperty name="dependency1" />
|
||||
|
||||
<cffunction name="get" hint="I am the function hint.">
|
||||
<cfargument name="id" hint="I am the argument hint." />
|
||||
<cfset local.headers = {}/>
|
||||
<cfset local.headers['x-dude'] = "dude!" />
|
||||
<cfset local.res = {} />
|
||||
<cfset local.res["id"] = arguments.id />
|
||||
<cfif structKeyExists(arguments, "dataFromOTR")>
|
||||
<cfset local.res.dataFromOTR = arguments.dataFromOTR />
|
||||
</cfif>
|
||||
<cfreturn representationOf(local.res).withStatus(999).withHeaders(local.headers) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="put">
|
||||
<cfargument name="id" />
|
||||
<cfreturn representationOf(arguments).withStatus(200) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="post">
|
||||
<cfargument name="id" />
|
||||
<cfreturn representationOf(arguments).withStatus(200) />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,38 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo/tunnel/{id}">
|
||||
|
||||
<cffunction name="get">
|
||||
<cfargument name="id" />
|
||||
|
||||
<cfset var echo = {} />
|
||||
<cfset echo.actualMethod = "get" />
|
||||
|
||||
<cfreturn representationOf(echo) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="put">
|
||||
<cfargument name="id" />
|
||||
|
||||
<cfset var echo = {} />
|
||||
<cfset echo.actualMethod = "put" />
|
||||
|
||||
<cfreturn representationOf(echo).withStatus(200) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="post">
|
||||
<cfargument name="id" />
|
||||
|
||||
<cfset var echo = {} />
|
||||
<cfset echo.actualMethod = "post" />
|
||||
|
||||
<cfreturn representationOf(echo).withStatus(201) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="delete">
|
||||
<cfargument name="id" />
|
||||
|
||||
<cfset var echo = {} />
|
||||
<cfset echo.actualMethod = "delete" />
|
||||
|
||||
<cfreturn representationOf(echo).withStatus(200) />
|
||||
</cffunction>
|
||||
</cfcomponent>
|
||||
@@ -1,26 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo_regex/{id:\d{5}}">
|
||||
|
||||
<cffunction name="get">
|
||||
<cfargument name="id" />
|
||||
<cfset local.headers = {}/>
|
||||
<cfset local.headers['x-dude'] = "dude!" />
|
||||
<cfreturn representationOf(arguments).withStatus(999).withHeaders(local.headers) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="put">
|
||||
<cfargument name="id" />
|
||||
<cfreturn representationOf(arguments).withStatus(200) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="post">
|
||||
<cfargument name="id" />
|
||||
<cfreturn representationOf(arguments).withStatus(200) />
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction name="setDependency2">
|
||||
<cfargument name="dependency2" />
|
||||
<cfset this.dependency2 = arguments.dependency2 />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,21 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo_alias/{id}, /echo_alias" hint="I have multiple uris">
|
||||
|
||||
<cffunction name="get">
|
||||
<cfargument name="id" default="0" />
|
||||
<cfset local.res = {} />
|
||||
<cfset local.res.id = arguments.id />
|
||||
|
||||
<cfreturn representationOf(local.res) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="put">
|
||||
<cfargument name="id" />
|
||||
<cfreturn representationOf(arguments).withStatus(200) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="post">
|
||||
<cfargument name="id" />
|
||||
<cfreturn representationOf(arguments).withStatus(200) />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -0,0 +1,14 @@
|
||||
component extends="core.resource" taffy:uri="/users/{userId}/orders/{orderId}" {
|
||||
|
||||
/**
|
||||
* GET /users/{userId}/orders/{orderId} - Returns order for a user
|
||||
*/
|
||||
function get(required string userId, required string orderId) {
|
||||
return rep({
|
||||
userId: arguments.userId,
|
||||
orderId: arguments.orderId,
|
||||
method: "GET"
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
component extends="core.resource" taffy:uri="/simple" {
|
||||
|
||||
/**
|
||||
* GET /simple - Returns a simple response
|
||||
*/
|
||||
function get() {
|
||||
return rep({ message: "Hello from simple resource", method: "GET" });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /simple - Creates something
|
||||
*/
|
||||
function post(string name = "") {
|
||||
return rep({ message: "Created", name: arguments.name, method: "POST" })
|
||||
.withStatus(201, "Created");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
component extends="core.resource" taffy:uri="/items/{id}" {
|
||||
|
||||
/**
|
||||
* GET /items/{id} - Returns an item by ID
|
||||
*/
|
||||
function get(required string id) {
|
||||
return rep({ id: arguments.id, name: "Item #arguments.id#", method: "GET" });
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /items/{id} - Updates an item
|
||||
*/
|
||||
function put(required string id, string name = "") {
|
||||
return rep({ id: arguments.id, name: arguments.name, updated: true, method: "PUT" });
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /items/{id} - Deletes an item
|
||||
*/
|
||||
function delete(required string id) {
|
||||
return rep({ id: arguments.id, deleted: true, method: "DELETE" });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
component extends="core.resource" taffy:uri="/custom" {
|
||||
|
||||
/**
|
||||
* Standard GET method
|
||||
*/
|
||||
function get() {
|
||||
return rep({ message: "Standard GET", method: "GET" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom PATCH method using taffy_verb metadata
|
||||
*/
|
||||
function updatePartial() taffy_verb="PATCH" {
|
||||
return rep({ message: "Custom PATCH handler", method: "PATCH" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom OPTIONS method using taffy_verb metadata (underscore style)
|
||||
*/
|
||||
function handleOptions() taffy_verb="OPTIONS" {
|
||||
return rep({ message: "Custom OPTIONS handler", method: "OPTIONS" });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/upload">
|
||||
|
||||
<cffunction name="post">
|
||||
<cfargument name="img" />
|
||||
<cfset var local = structNew() />
|
||||
|
||||
<!--- accept the upload --->
|
||||
<cffile action="upload" destination="#getTempDirectory()#" filefield="img" result="local.r" />
|
||||
|
||||
<!--- delete the file --->
|
||||
<cffile action="delete" file="#local.r.serverdirectory#/#local.r.serverfile#" />
|
||||
|
||||
<cfreturn representationOf( local.r ).withStatus(200) />
|
||||
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,22 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.nativeJsonSerializer">
|
||||
|
||||
<cfproperty name="dependency1" />
|
||||
|
||||
<cffunction name="getAsJSON" output="false"
|
||||
taffy_mime="text/json"
|
||||
taffy_default="true"
|
||||
hint="serializes data as JSON">
|
||||
|
||||
<cfreturn super.getAsJson() />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getAsXML" output="false" taffy:mime="text/xml">
|
||||
<cfreturn "FAIL FAIL FAIL" />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="setDependency2">
|
||||
<cfargument name="dependency2" />
|
||||
<cfset this.dependency2 = arguments.dependency2 />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1 +0,0 @@
|
||||
<cfcomponent></cfcomponent>
|
||||
@@ -1 +0,0 @@
|
||||
<cfcomponent></cfcomponent>
|
||||
@@ -1,7 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/throwException">
|
||||
|
||||
<cffunction name="get">
|
||||
<cfthrow message="this is the exception message." detail="this is the exception detail." />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,7 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/uriWithForwardSlash{,},uriAliasWithoutFowardSlash">
|
||||
|
||||
<cffunction name="get">
|
||||
<cfreturn noData() />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,7 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo/towel">
|
||||
|
||||
<cffunction name="get">
|
||||
<cfreturn representationOf("don't panic!").withStatus(200) />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,7 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource" taffy:uri="uriWithoutForwardSlash">
|
||||
|
||||
<cffunction name="get">
|
||||
<cfreturn noData() />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,5 +0,0 @@
|
||||
<cfcomponent extends="taffy.core.resource">
|
||||
<cfif structKeyExists(request, "_testsRunning")>
|
||||
<cfthrow message="Fail">
|
||||
</cfif>
|
||||
</cfcomponent>
|
||||
@@ -0,0 +1,23 @@
|
||||
<cfscript>
|
||||
/**
|
||||
* TestBox Runner for Taffy Framework Tests
|
||||
*
|
||||
* Run this file via browser or use CommandBox:
|
||||
* box testbox run
|
||||
*/
|
||||
|
||||
// Create TestBox instance
|
||||
testBox = new testbox.system.TestBox();
|
||||
|
||||
// Run all specs in the specs directory
|
||||
results = testBox.run(
|
||||
directory = {
|
||||
recurse: true,
|
||||
mapping: "tests.specs"
|
||||
},
|
||||
reporter = url.reporter ?: "simple"
|
||||
);
|
||||
|
||||
// Output the results
|
||||
writeOutput(results);
|
||||
</cfscript>
|
||||
@@ -1 +0,0 @@
|
||||
I am some non-framework-related file. woot.
|
||||
@@ -0,0 +1,173 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function beforeAll() {
|
||||
// Build base URL from the server running these tests
|
||||
var port = cgi.server_port;
|
||||
var host = cgi.server_name;
|
||||
if (!len(host) || host == "") host = "localhost";
|
||||
variables.baseURL = "http://#host#:#port#/tests/testapi/index.cfm";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: make an HTTP request to the test API
|
||||
*/
|
||||
private struct function apiRequest(
|
||||
required string uri,
|
||||
string method = "GET",
|
||||
string body = "",
|
||||
struct headers = {}
|
||||
) {
|
||||
var result = {};
|
||||
var fullURL = variables.baseURL & "?endpoint=" & arguments.uri;
|
||||
|
||||
cfhttp(url=fullURL, method=arguments.method, result="result", timeout=10, charset="utf-8") {
|
||||
cfhttpparam(type="header", name="Content-Type", value="application/json");
|
||||
cfhttpparam(type="header", name="Accept", value="application/json");
|
||||
for (var h in arguments.headers) {
|
||||
cfhttpparam(type="header", name=h, value=arguments.headers[h]);
|
||||
}
|
||||
if (len(arguments.body)) {
|
||||
cfhttpparam(type="body", value=arguments.body);
|
||||
}
|
||||
}
|
||||
|
||||
result.data = {};
|
||||
if (structKeyExists(result, "fileContent")) {
|
||||
var body = result.fileContent;
|
||||
if (!isSimpleValue(body)) {
|
||||
body = toString(body);
|
||||
}
|
||||
body = trim(body);
|
||||
if (len(body) && isJSON(body)) {
|
||||
result.data = deserializeJSON(body);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
describe("End-to-End HTTP Tests", function() {
|
||||
|
||||
describe("GET requests", function() {
|
||||
|
||||
it("should return 200 for a valid GET request", function() {
|
||||
var res = apiRequest("/echo");
|
||||
expect(res.status_code).toBe(200);
|
||||
expect(res.data.method).toBe("GET");
|
||||
expect(res.data.message).toBe("echo");
|
||||
});
|
||||
|
||||
it("should extract URI token from path", function() {
|
||||
var res = apiRequest("/echo/42");
|
||||
expect(res.status_code).toBe(200);
|
||||
expect(res.data.id).toBe("42");
|
||||
expect(res.data.method).toBe("GET");
|
||||
});
|
||||
|
||||
it("should extract multiple URI tokens", function() {
|
||||
var res = apiRequest("/echo/10/child/20");
|
||||
expect(res.status_code).toBe(200);
|
||||
expect(res.data.parentId).toBe("10");
|
||||
expect(res.data.childId).toBe("20");
|
||||
});
|
||||
|
||||
it("should return JSON content type", function() {
|
||||
var res = apiRequest("/echo");
|
||||
expect(res.responseheader["Content-Type"]).toInclude("application/json");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("POST requests", function() {
|
||||
|
||||
it("should return 201 Created", function() {
|
||||
var res = apiRequest(
|
||||
uri = "/echo",
|
||||
method = "POST",
|
||||
body = '{"name":"test","value":"123"}'
|
||||
);
|
||||
expect(res.status_code).toBe(201);
|
||||
expect(res.data.method).toBe("POST");
|
||||
});
|
||||
|
||||
it("should deserialize JSON request body", function() {
|
||||
var res = apiRequest(
|
||||
uri = "/echo",
|
||||
method = "POST",
|
||||
body = '{"name":"widget","value":"abc"}'
|
||||
);
|
||||
expect(res.data.name).toBe("widget");
|
||||
expect(res.data.value).toBe("abc");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("PUT requests", function() {
|
||||
|
||||
it("should handle PUT with URI token and body", function() {
|
||||
var res = apiRequest(
|
||||
uri = "/echo/55",
|
||||
method = "PUT",
|
||||
body = '{"name":"updated"}'
|
||||
);
|
||||
expect(res.status_code).toBe(200);
|
||||
expect(res.data.method).toBe("PUT");
|
||||
expect(res.data.id).toBe("55");
|
||||
expect(res.data.name).toBe("updated");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("DELETE requests", function() {
|
||||
|
||||
it("should handle DELETE with URI token", function() {
|
||||
var res = apiRequest(uri="/echo/99", method="DELETE");
|
||||
expect(res.status_code).toBe(200);
|
||||
expect(res.data.method).toBe("DELETE");
|
||||
expect(res.data.id).toBe("99");
|
||||
});
|
||||
|
||||
it("should return custom response headers", function() {
|
||||
var res = apiRequest(uri="/echo/99", method="DELETE");
|
||||
expect(res.responseheader["X-Deleted-Id"]).toBe("99");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Error responses", function() {
|
||||
|
||||
it("should return 404 for unknown URI", function() {
|
||||
var res = apiRequest("/nonexistent/path");
|
||||
expect(res.status_code).toBe(404);
|
||||
});
|
||||
|
||||
it("should return 405 for unsupported HTTP method", function() {
|
||||
// /echo/{parentId}/child/{childId} only supports GET
|
||||
var res = apiRequest(uri="/echo/1/child/2", method="DELETE");
|
||||
expect(res.status_code).toBe(405);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("X-HTTP-Method-Override", function() {
|
||||
|
||||
it("should support method tunneling via header", function() {
|
||||
var res = apiRequest(
|
||||
uri = "/echo/77",
|
||||
method = "POST",
|
||||
headers = { "X-HTTP-Method-Override": "PUT" },
|
||||
body = '{"name":"tunneled"}'
|
||||
);
|
||||
expect(res.data.method).toBe("PUT");
|
||||
expect(res.data.id).toBe("77");
|
||||
expect(res.data.name).toBe("tunneled");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function beforeAll() {
|
||||
var port = cgi.server_port;
|
||||
var host = cgi.server_name;
|
||||
if (!len(host) || host == "") host = "localhost";
|
||||
variables.baseURL = "http://#host#:#port#/tests/testapi/index.cfm";
|
||||
}
|
||||
|
||||
private struct function fetchSpec(string param = "openapi") {
|
||||
var result = {};
|
||||
cfhttp(url="#variables.baseURL#?#arguments.param#", method="GET", result="result", timeout=10, charset="utf-8");
|
||||
return result;
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
describe("OpenAPI 3.1 spec endpoint", function() {
|
||||
|
||||
it("returns JSON with openapi version 3.1.0", function() {
|
||||
var res = fetchSpec("openapi");
|
||||
expect(res.status_code).toBe(200);
|
||||
expect(isJSON(res.fileContent)).toBeTrue("Expected JSON, got: " & left(res.fileContent, 300));
|
||||
var spec = deserializeJSON(res.fileContent);
|
||||
expect(spec.openapi).toBe("3.1.0");
|
||||
});
|
||||
|
||||
it("includes info.title and info.version", function() {
|
||||
var spec = deserializeJSON(fetchSpec().fileContent);
|
||||
expect(spec).toHaveKey("info");
|
||||
expect(spec.info).toHaveKey("title");
|
||||
expect(spec.info).toHaveKey("version");
|
||||
});
|
||||
|
||||
it("documents /echo/{id} with GET, PUT, DELETE", function() {
|
||||
var spec = deserializeJSON(fetchSpec().fileContent);
|
||||
expect(spec).toHaveKey("paths");
|
||||
expect(spec.paths).toHaveKey("/echo/{id}");
|
||||
var echo = spec.paths["/echo/{id}"];
|
||||
expect(echo).toHaveKey("get");
|
||||
expect(echo).toHaveKey("put");
|
||||
expect(echo).toHaveKey("delete");
|
||||
});
|
||||
|
||||
it("marks path token 'id' as required in:path on GET", function() {
|
||||
var spec = deserializeJSON(fetchSpec().fileContent);
|
||||
var params = spec.paths["/echo/{id}"].get.parameters;
|
||||
var found = false;
|
||||
for (var i = 1; i <= arrayLen(params); i++) {
|
||||
if (params[i].name == "id" && params[i]["in"] == "path" && params[i].required == true) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(found).toBeTrue("Expected required path parameter 'id' on GET /echo/{id}");
|
||||
});
|
||||
|
||||
it("emits requestBody with json + form-urlencoded for PUT body params", function() {
|
||||
var spec = deserializeJSON(fetchSpec().fileContent);
|
||||
var put = spec.paths["/echo/{id}"].put;
|
||||
expect(put).toHaveKey("requestBody");
|
||||
expect(put.requestBody.content).toHaveKey("application/json");
|
||||
expect(put.requestBody.content).toHaveKey("application/x-www-form-urlencoded");
|
||||
var props = put.requestBody.content["application/json"].schema.properties;
|
||||
expect(props).toHaveKey("name");
|
||||
});
|
||||
|
||||
it("includes responses.200 for each operation", function() {
|
||||
var spec = deserializeJSON(fetchSpec().fileContent);
|
||||
expect(spec.paths["/echo/{id}"].get).toHaveKey("responses");
|
||||
expect(spec.paths["/echo/{id}"].get.responses).toHaveKey("200");
|
||||
});
|
||||
|
||||
it("accepts ?swagger alias", function() {
|
||||
var res = fetchSpec("swagger");
|
||||
expect(res.status_code).toBe(200);
|
||||
expect(deserializeJSON(res.fileContent).openapi).toBe("3.1.0");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function beforeAll() {
|
||||
// Save existing application context so we can restore it after
|
||||
if (structKeyExists(application, "_taffy")) {
|
||||
variables.savedTaffy = duplicate(application._taffy);
|
||||
}
|
||||
// Create a fresh API instance and bootstrap it to populate application._taffy with defaults
|
||||
variables.api = new core.api();
|
||||
variables.api.onApplicationStart();
|
||||
variables.settings = application._taffy.settings;
|
||||
}
|
||||
|
||||
function afterAll() {
|
||||
// Restore original application context
|
||||
if (structKeyExists(variables, "savedTaffy")) {
|
||||
application._taffy = variables.savedTaffy;
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
describe("Configuration", function() {
|
||||
|
||||
describe("Default settings", function() {
|
||||
|
||||
it("should have reloadKey default of 'reload'", function() {
|
||||
expect(settings.reloadKey).toBe("reload");
|
||||
});
|
||||
|
||||
it("should have reloadPassword default of 'true'", function() {
|
||||
expect(settings.reloadPassword).toBe("true");
|
||||
});
|
||||
|
||||
it("should have reloadOnEveryRequest default of false", function() {
|
||||
expect(settings.reloadOnEveryRequest).toBeFalse();
|
||||
});
|
||||
|
||||
it("should have disableDashboard default of false", function() {
|
||||
expect(settings.disableDashboard).toBeFalse();
|
||||
});
|
||||
|
||||
it("should have disabledDashboardRedirect default of empty string", function() {
|
||||
expect(settings.disabledDashboardRedirect).toBe("");
|
||||
});
|
||||
|
||||
it("should have showDocsWhenDashboardDisabled default of false", function() {
|
||||
expect(settings.showDocsWhenDashboardDisabled).toBeFalse();
|
||||
});
|
||||
|
||||
it("should have returnExceptionsAsJson default of false", function() {
|
||||
expect(settings.returnExceptionsAsJson).toBeFalse();
|
||||
});
|
||||
|
||||
it("should have exposeTaffyHeaders default of true", function() {
|
||||
expect(settings.exposeTaffyHeaders).toBeTrue();
|
||||
});
|
||||
|
||||
it("should have allowCrossDomain default of false", function() {
|
||||
expect(settings.allowCrossDomain).toBeFalse();
|
||||
});
|
||||
|
||||
it("should have useEtags default of false", function() {
|
||||
expect(settings.useEtags).toBeFalse();
|
||||
});
|
||||
|
||||
it("should have jsonp default of false", function() {
|
||||
expect(settings.jsonp).toBeFalse();
|
||||
});
|
||||
|
||||
it("should have noDataSends204NoContent default of false", function() {
|
||||
expect(settings.noDataSends204NoContent).toBeFalse();
|
||||
});
|
||||
|
||||
it("should have endpointURLParam default of 'endpoint'", function() {
|
||||
expect(settings.endpointURLParam).toBe("endpoint");
|
||||
});
|
||||
|
||||
it("should have debugKey default of 'debug'", function() {
|
||||
expect(settings.debugKey).toBe("debug");
|
||||
});
|
||||
|
||||
it("should have unhandledPaths default of '/flex2gateway'", function() {
|
||||
expect(settings.unhandledPaths).toBe("/flex2gateway");
|
||||
});
|
||||
|
||||
it("should have globalHeaders default to empty struct", function() {
|
||||
expect(settings.globalHeaders).toBeStruct();
|
||||
expect(structIsEmpty(settings.globalHeaders)).toBeTrue();
|
||||
});
|
||||
|
||||
it("should have exceptionLogAdapter default of LogToDevNull", function() {
|
||||
expect(settings.exceptionLogAdapter).toBe("taffy.bonus.LogToDevNull");
|
||||
});
|
||||
|
||||
it("should have allowGoogleFonts default of true", function() {
|
||||
expect(settings.allowGoogleFonts).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Serializer configuration", function() {
|
||||
|
||||
it("should use nativeJsonSerializer by default", function() {
|
||||
expect(settings.serializer).toBe("taffy.core.nativeJsonSerializer");
|
||||
});
|
||||
|
||||
it("should have nativeJsonSerializer extend baseSerializer", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(serializer, "baseSerializer")).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Deserializer configuration", function() {
|
||||
|
||||
it("should use nativeJsonDeserializer by default", function() {
|
||||
expect(settings.deserializer).toBe("taffy.core.nativeJsonDeserializer");
|
||||
});
|
||||
|
||||
it("should have nativeJsonDeserializer extend baseDeserializer", function() {
|
||||
var deserializer = new core.nativeJsonDeserializer();
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(deserializer, "baseDeserializer")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should support JSON content type via getFromJson", function() {
|
||||
var deserializer = new core.nativeJsonDeserializer();
|
||||
var meta = getMetadata(deserializer);
|
||||
var hasJsonSupport = false;
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "getFromJson") {
|
||||
hasJsonSupport = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(hasJsonSupport).toBeTrue();
|
||||
});
|
||||
|
||||
it("should support form-urlencoded content type via getFromForm", function() {
|
||||
var deserializer = new core.baseDeserializer();
|
||||
var meta = getMetadata(deserializer);
|
||||
var hasFormSupport = false;
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "getFromForm") {
|
||||
hasFormSupport = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(hasFormSupport).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("MIME type configuration", function() {
|
||||
|
||||
it("should detect mime types from serializer metadata", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
var meta = getMetadata(serializer);
|
||||
|
||||
var jsonMime = "";
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "getAsJson") {
|
||||
if (structKeyExists(func, "taffy:mime")) {
|
||||
jsonMime = func["taffy:mime"];
|
||||
} else if (structKeyExists(func, "taffy_mime")) {
|
||||
jsonMime = func["taffy_mime"];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(jsonMime).toInclude("application/json");
|
||||
});
|
||||
|
||||
it("should identify default mime type from serializer", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
var meta = getMetadata(serializer);
|
||||
|
||||
var isDefault = false;
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "getAsJson") {
|
||||
if (structKeyExists(func, "taffy:default")) {
|
||||
isDefault = func["taffy:default"];
|
||||
} else if (structKeyExists(func, "taffy_default")) {
|
||||
isDefault = func["taffy_default"];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(isDefault).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Version", function() {
|
||||
|
||||
it("should report version 4.0.0", function() {
|
||||
expect(application._taffy.version).toBe("4.0.0");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Unhandled paths regex", function() {
|
||||
|
||||
it("should convert unhandledPaths to regex", function() {
|
||||
expect(structKeyExists(settings, "unhandledPathsRegex")).toBeTrue();
|
||||
expect(len(settings.unhandledPathsRegex)).toBeGT(0);
|
||||
});
|
||||
|
||||
it("should match the default /flex2gateway path", function() {
|
||||
expect(reFindNoCase("^(" & settings.unhandledPathsRegex & ")", "/flex2gateway")).toBeGT(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Content types", function() {
|
||||
|
||||
it("should have registered supported content types", function() {
|
||||
expect(structKeyExists(application._taffy, "contentTypes")).toBeTrue();
|
||||
expect(application._taffy.contentTypes).toBeStruct();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function beforeAll() {
|
||||
// Initialize application scope with minimal settings required by resource.cfc
|
||||
application._taffy = {
|
||||
settings: {
|
||||
serializer: "core.nativeJsonSerializer",
|
||||
noDataSends204NoContent: false
|
||||
},
|
||||
factory: new core.factory(),
|
||||
compat: {
|
||||
queryToArray: "missing",
|
||||
queryToStruct: "missing"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
describe("HTTP Methods", function() {
|
||||
|
||||
describe("Standard HTTP verb mapping", function() {
|
||||
|
||||
it("should recognize GET method in resource", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
var meta = getMetadata(resource);
|
||||
var hasGet = false;
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "get") {
|
||||
hasGet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(hasGet).toBeTrue();
|
||||
});
|
||||
|
||||
it("should recognize POST method in resource", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
var meta = getMetadata(resource);
|
||||
var hasPost = false;
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "post") {
|
||||
hasPost = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(hasPost).toBeTrue();
|
||||
});
|
||||
|
||||
it("should recognize PUT method in resource", function() {
|
||||
var resource = new tests.resources.MockTokenResource();
|
||||
var meta = getMetadata(resource);
|
||||
var hasPut = false;
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "put") {
|
||||
hasPut = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(hasPut).toBeTrue();
|
||||
});
|
||||
|
||||
it("should recognize DELETE method in resource", function() {
|
||||
var resource = new tests.resources.MockTokenResource();
|
||||
var meta = getMetadata(resource);
|
||||
var hasDelete = false;
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "delete") {
|
||||
hasDelete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(hasDelete).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("taffy:verb metadata override", function() {
|
||||
|
||||
it("should recognize taffy:verb=PATCH on custom method", function() {
|
||||
var resource = new tests.resources.MockVerbOverrideResource();
|
||||
var meta = getMetadata(resource);
|
||||
var patchFunc = "";
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "updatePartial") {
|
||||
patchFunc = func;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(patchFunc).notToBeEmpty();
|
||||
var hasVerbMeta = structKeyExists(patchFunc, "taffy:verb")
|
||||
|| structKeyExists(patchFunc, "taffy_verb");
|
||||
expect(hasVerbMeta).toBeTrue();
|
||||
|
||||
var verbValue = structKeyExists(patchFunc, "taffy:verb")
|
||||
? patchFunc["taffy:verb"]
|
||||
: patchFunc["taffy_verb"];
|
||||
expect(verbValue).toBe("PATCH");
|
||||
});
|
||||
|
||||
it("should recognize taffy_verb (underscore style) on custom method", function() {
|
||||
var resource = new tests.resources.MockVerbOverrideResource();
|
||||
var meta = getMetadata(resource);
|
||||
var optionsFunc = "";
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "handleOptions") {
|
||||
optionsFunc = func;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(optionsFunc).notToBeEmpty();
|
||||
var hasVerbMeta = structKeyExists(optionsFunc, "taffy:verb")
|
||||
|| structKeyExists(optionsFunc, "taffy_verb");
|
||||
expect(hasVerbMeta).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Resource taffy:uri metadata", function() {
|
||||
|
||||
it("should have taffy:uri on MockSimpleResource", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
var meta = getMetadata(resource);
|
||||
var hasUri = structKeyExists(meta, "taffy:uri")
|
||||
|| structKeyExists(meta, "taffy_uri");
|
||||
expect(hasUri).toBeTrue();
|
||||
});
|
||||
|
||||
it("should have correct URI pattern on MockTokenResource", function() {
|
||||
var resource = new tests.resources.MockTokenResource();
|
||||
var meta = getMetadata(resource);
|
||||
var uri = structKeyExists(meta, "taffy:uri")
|
||||
? meta["taffy:uri"]
|
||||
: meta["taffy_uri"];
|
||||
expect(uri).toBe("/items/{id}");
|
||||
});
|
||||
|
||||
it("should have correct URI pattern on MockMultiTokenResource", function() {
|
||||
var resource = new tests.resources.MockMultiTokenResource();
|
||||
var meta = getMetadata(resource);
|
||||
var uri = structKeyExists(meta, "taffy:uri")
|
||||
? meta["taffy:uri"]
|
||||
: meta["taffy_uri"];
|
||||
expect(uri).toBe("/users/{userId}/orders/{orderId}");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Resource inheritance", function() {
|
||||
|
||||
it("should extend core.resource", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(resource, "resource")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should have access to rep() method from parent", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
expect(resource).toHaveKey("rep");
|
||||
});
|
||||
|
||||
it("should have access to representationOf() method from parent", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
expect(resource).toHaveKey("representationOf");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Resource method invocation", function() {
|
||||
|
||||
it("should execute GET and return serializer", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
var result = resource.get();
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
expect(result.getData().method).toBe("GET");
|
||||
});
|
||||
|
||||
it("should execute POST with arguments", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
var result = resource.post(name = "TestItem");
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
expect(result.getData().name).toBe("TestItem");
|
||||
expect(result.getStatus()).toBe(201);
|
||||
});
|
||||
|
||||
it("should execute GET with token parameter", function() {
|
||||
var resource = new tests.resources.MockTokenResource();
|
||||
var result = resource.get(id = "123");
|
||||
expect(result.getData().id).toBe("123");
|
||||
expect(result.getData().method).toBe("GET");
|
||||
});
|
||||
|
||||
it("should execute PUT with token and body parameters", function() {
|
||||
var resource = new tests.resources.MockTokenResource();
|
||||
var result = resource.put(id = "456", name = "Updated");
|
||||
expect(result.getData().id).toBe("456");
|
||||
expect(result.getData().name).toBe("Updated");
|
||||
expect(result.getData().updated).toBeTrue();
|
||||
});
|
||||
|
||||
it("should execute DELETE with token parameter", function() {
|
||||
var resource = new tests.resources.MockTokenResource();
|
||||
var result = resource.delete(id = "789");
|
||||
expect(result.getData().id).toBe("789");
|
||||
expect(result.getData().deleted).toBeTrue();
|
||||
});
|
||||
|
||||
it("should execute GET with multiple tokens", function() {
|
||||
var resource = new tests.resources.MockMultiTokenResource();
|
||||
var result = resource.get(userId = "1", orderId = "2");
|
||||
expect(result.getData().userId).toBe("1");
|
||||
expect(result.getData().orderId).toBe("2");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function run() {
|
||||
|
||||
describe("Hooks", function() {
|
||||
|
||||
describe("onTaffyRequest hook", function() {
|
||||
|
||||
it("should be defined in api.cfc", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("onTaffyRequest");
|
||||
});
|
||||
|
||||
it("should accept required parameters", function() {
|
||||
var api = new core.api();
|
||||
var meta = getMetadata(api);
|
||||
var hookFunc = "";
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "onTaffyRequest") {
|
||||
hookFunc = func;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(hookFunc).notToBeEmpty();
|
||||
expect(hookFunc.parameters).toBeArray();
|
||||
|
||||
// Should have all expected parameters
|
||||
var paramNames = [];
|
||||
for (var param in hookFunc.parameters) {
|
||||
arrayAppend(paramNames, param.name);
|
||||
}
|
||||
expect(arrayFindNoCase(paramNames, "verb")).toBeGT(0);
|
||||
expect(arrayFindNoCase(paramNames, "cfc")).toBeGT(0);
|
||||
expect(arrayFindNoCase(paramNames, "requestArguments")).toBeGT(0);
|
||||
expect(arrayFindNoCase(paramNames, "mimeExt")).toBeGT(0);
|
||||
expect(arrayFindNoCase(paramNames, "headers")).toBeGT(0);
|
||||
expect(arrayFindNoCase(paramNames, "methodMetadata")).toBeGT(0);
|
||||
expect(arrayFindNoCase(paramNames, "matchedURI")).toBeGT(0);
|
||||
});
|
||||
|
||||
it("should return true by default", function() {
|
||||
var api = new core.api();
|
||||
var result = api.onTaffyRequest(
|
||||
verb = "GET",
|
||||
cfc = "testResource",
|
||||
requestArguments = {},
|
||||
mimeExt = "json",
|
||||
headers = {},
|
||||
methodMetadata = {},
|
||||
matchedURI = "/test"
|
||||
);
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("onTaffyRequestEnd hook", function() {
|
||||
|
||||
it("should be defined in api.cfc", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("onTaffyRequestEnd");
|
||||
});
|
||||
|
||||
it("should accept required parameters", function() {
|
||||
var api = new core.api();
|
||||
var meta = getMetadata(api);
|
||||
var hookFunc = "";
|
||||
for (var func in meta.functions) {
|
||||
if (func.name == "onTaffyRequestEnd") {
|
||||
hookFunc = func;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(hookFunc).notToBeEmpty();
|
||||
|
||||
var paramNames = [];
|
||||
for (var param in hookFunc.parameters) {
|
||||
arrayAppend(paramNames, param.name);
|
||||
}
|
||||
|
||||
// Should include response-related parameters
|
||||
expect(arrayFindNoCase(paramNames, "parsedResponse")).toBeGT(0);
|
||||
expect(arrayFindNoCase(paramNames, "originalResponse")).toBeGT(0);
|
||||
expect(arrayFindNoCase(paramNames, "statusCode")).toBeGT(0);
|
||||
});
|
||||
|
||||
it("should return true by default", function() {
|
||||
var api = new core.api();
|
||||
var result = api.onTaffyRequestEnd(
|
||||
verb = "GET",
|
||||
cfc = "testResource",
|
||||
requestArguments = {},
|
||||
mimeExt = "json",
|
||||
headers = {},
|
||||
methodMetadata = {},
|
||||
matchedURI = "/test",
|
||||
parsedResponse = '{"test":true}',
|
||||
originalResponse = { test: true },
|
||||
statusCode = 200
|
||||
);
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Caching hooks", function() {
|
||||
|
||||
it("should have validCacheExists hook", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("validCacheExists");
|
||||
});
|
||||
|
||||
it("validCacheExists should return false by default", function() {
|
||||
var api = new core.api();
|
||||
var result = api.validCacheExists(cacheKey = "test-key");
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
it("should have setCachedResponse hook", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("setCachedResponse");
|
||||
});
|
||||
|
||||
it("should have getCachedResponse hook", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("getCachedResponse");
|
||||
});
|
||||
|
||||
it("should have getCacheKey hook", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("getCacheKey");
|
||||
});
|
||||
|
||||
it("getCacheKey should generate key from request params", function() {
|
||||
var api = new core.api();
|
||||
var requestArgs = { id: 123 };
|
||||
var result = api.getCacheKey(
|
||||
cfc = "testResource",
|
||||
requestArguments = requestArgs,
|
||||
matchedURI = "/items/{id}"
|
||||
);
|
||||
expect(result).toInclude("/items/{id}");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("getEnvironment hook", function() {
|
||||
|
||||
it("should be defined in api.cfc", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("getEnvironment");
|
||||
});
|
||||
|
||||
it("should return empty string by default", function() {
|
||||
var api = new core.api();
|
||||
var result = api.getEnvironment();
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Hook extensibility", function() {
|
||||
|
||||
it("should allow overriding onTaffyRequest in subclass", function() {
|
||||
// Any class extending core.api can override hooks
|
||||
var api = new core.api();
|
||||
// The base api has the hook that can be overridden
|
||||
expect(api).toHaveKey("onTaffyRequest");
|
||||
// Verify it's a function that can be overridden
|
||||
expect(isCustomFunction(api.onTaffyRequest)).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Basic auth helper", function() {
|
||||
|
||||
it("should have getBasicAuthCredentials method", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("getBasicAuthCredentials");
|
||||
});
|
||||
|
||||
it("getBasicAuthCredentials should return struct with username and password", function() {
|
||||
var api = new core.api();
|
||||
var result = api.getBasicAuthCredentials();
|
||||
expect(result).toBeStruct();
|
||||
expect(result).toHaveKey("username");
|
||||
expect(result).toHaveKey("password");
|
||||
});
|
||||
|
||||
it("should return empty credentials when no auth header", function() {
|
||||
var api = new core.api();
|
||||
var result = api.getBasicAuthCredentials();
|
||||
expect(result.username).toBe("");
|
||||
expect(result.password).toBe("");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Helper methods", function() {
|
||||
|
||||
it("should have getHostname helper", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("getHostname");
|
||||
var hostname = api.getHostname();
|
||||
expect(len(hostname)).toBeGT(0);
|
||||
});
|
||||
|
||||
it("should have addHeaders helper", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("addHeaders");
|
||||
});
|
||||
|
||||
it("should have rep helper for responses", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("rep");
|
||||
});
|
||||
|
||||
it("should have representationOf helper", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("representationOf");
|
||||
});
|
||||
|
||||
it("should have noData helper", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("noData");
|
||||
});
|
||||
|
||||
it("should have noContent helper", function() {
|
||||
var api = new core.api();
|
||||
expect(api).toHaveKey("noContent");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function beforeAll() {
|
||||
// Set up minimal application context
|
||||
application._taffy = {
|
||||
settings: {
|
||||
serializer: "core.nativeJsonSerializer",
|
||||
noDataSends204NoContent: false
|
||||
},
|
||||
factory: new core.factory(),
|
||||
compat: {
|
||||
queryToArray: "missing",
|
||||
queryToStruct: "missing"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
describe("Response Handling", function() {
|
||||
|
||||
describe("Status codes", function() {
|
||||
|
||||
it("should default to 200 OK", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
var result = resource.get();
|
||||
expect(result.getStatus()).toBe(200);
|
||||
expect(result.getStatusText()).toBe("OK");
|
||||
});
|
||||
|
||||
it("should support 201 Created", function() {
|
||||
var resource = new tests.resources.MockSimpleResource();
|
||||
var result = resource.post(name = "Test");
|
||||
expect(result.getStatus()).toBe(201);
|
||||
expect(result.getStatusText()).toBe("Created");
|
||||
});
|
||||
|
||||
it("should support 404 Not Found", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData({ error: "Resource not found" }).withStatus(404);
|
||||
expect(serializer.getStatus()).toBe(404);
|
||||
expect(serializer.getStatusText()).toBe("Not Found");
|
||||
});
|
||||
|
||||
it("should support 400 Bad Request", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData({ error: "Invalid input" }).withStatus(400);
|
||||
expect(serializer.getStatus()).toBe(400);
|
||||
expect(serializer.getStatusText()).toBe("Bad Request");
|
||||
});
|
||||
|
||||
it("should support 500 Internal Server Error", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData({ error: "Something went wrong" }).withStatus(500);
|
||||
expect(serializer.getStatus()).toBe(500);
|
||||
expect(serializer.getStatusText()).toBe("Internal Server Error");
|
||||
});
|
||||
|
||||
it("should support custom status text", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.withStatus(299, "Custom Success");
|
||||
expect(serializer.getStatus()).toBe(299);
|
||||
expect(serializer.getStatusText()).toBe("Custom Success");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Response headers", function() {
|
||||
|
||||
it("should set custom headers", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData({ id: 123 })
|
||||
.withHeaders({ "X-Request-Id": "abc123" });
|
||||
var headers = serializer.getHeaders();
|
||||
expect(headers["X-Request-Id"]).toBe("abc123");
|
||||
});
|
||||
|
||||
it("should set Location header for created resources", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData({ id: 456 })
|
||||
.withStatus(201)
|
||||
.withHeaders({ "Location": "/items/456" });
|
||||
expect(serializer.getHeaders()["Location"]).toBe("/items/456");
|
||||
});
|
||||
|
||||
it("should set multiple headers", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.withHeaders({
|
||||
"X-Rate-Limit": "100",
|
||||
"X-Rate-Remaining": "99",
|
||||
"X-Cache-Control": "no-cache"
|
||||
});
|
||||
var headers = serializer.getHeaders();
|
||||
expect(headers["X-Rate-Limit"]).toBe("100");
|
||||
expect(headers["X-Rate-Remaining"]).toBe("99");
|
||||
expect(headers["X-Cache-Control"]).toBe("no-cache");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("JSON serialization", function() {
|
||||
|
||||
it("should serialize response data as JSON", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData({ message: "Hello", count: 42 });
|
||||
var json = serializer.getAsJson();
|
||||
expect(isJSON(json)).toBeTrue();
|
||||
var parsed = deserializeJSON(json);
|
||||
expect(parsed.message).toBe("Hello");
|
||||
expect(parsed.count).toBe(42);
|
||||
});
|
||||
|
||||
it("should serialize nested structures", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData({
|
||||
user: {
|
||||
name: "John",
|
||||
email: "john@example.com"
|
||||
},
|
||||
permissions: ["read", "write"]
|
||||
});
|
||||
var json = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(json);
|
||||
expect(parsed.user.name).toBe("John");
|
||||
expect(parsed.permissions).toBeArray();
|
||||
});
|
||||
|
||||
it("should serialize array responses", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData([
|
||||
{ id: 1, name: "Item 1" },
|
||||
{ id: 2, name: "Item 2" }
|
||||
]);
|
||||
var json = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(json);
|
||||
expect(parsed).toBeArray();
|
||||
expect(arrayLen(parsed)).toBe(2);
|
||||
});
|
||||
|
||||
it("should handle boolean values correctly", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData({
|
||||
active: true,
|
||||
deleted: false
|
||||
});
|
||||
var json = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(json);
|
||||
expect(parsed.active).toBeTrue();
|
||||
expect(parsed.deleted).toBeFalse();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("noData() and noContent()", function() {
|
||||
|
||||
it("noData() should return empty response when setting is false", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setNoDataSends204NoContent(false);
|
||||
var result = serializer.noData();
|
||||
// Should still be 200 with empty data
|
||||
expect(result.getStatus()).toBe(200);
|
||||
});
|
||||
|
||||
it("noData() should return 204 when setting is true", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setNoDataSends204NoContent(true);
|
||||
var result = serializer.noData();
|
||||
expect(result.getStatus()).toBe(204);
|
||||
});
|
||||
|
||||
it("noContent() should return 204", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
var result = serializer.noContent();
|
||||
expect(result.getStatus()).toBe(204);
|
||||
expect(result.getStatusText()).toBe("No Content");
|
||||
});
|
||||
|
||||
it("noContent() should set Content-Type to text/plain", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
var result = serializer.noContent();
|
||||
var headers = result.getHeaders();
|
||||
expect(headers["Content-Type"]).toBe("text/plain");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Response type detection", function() {
|
||||
|
||||
it("should identify textual response type", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setData({ message: "test" });
|
||||
expect(serializer.getType()).toBe("textual");
|
||||
});
|
||||
|
||||
it("should identify filename response type", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setFileName("/path/to/file.pdf");
|
||||
expect(serializer.getType()).toBe("filename");
|
||||
});
|
||||
|
||||
it("should identify filedata response type", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setFileData(toBinary(toBase64("test data")));
|
||||
expect(serializer.getType()).toBe("filedata");
|
||||
});
|
||||
|
||||
it("should identify imagedata response type", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setImageData(toBinary(toBase64("image data")));
|
||||
expect(serializer.getType()).toBe("imagedata");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("File streaming responses", function() {
|
||||
|
||||
it("should configure file download response", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setFileName("/path/to/report.pdf")
|
||||
.withMime("application/pdf");
|
||||
|
||||
expect(serializer.getFileName()).toBe("/path/to/report.pdf");
|
||||
expect(serializer.getFileMime()).toBe("application/pdf");
|
||||
expect(serializer.getType()).toBe("filename");
|
||||
});
|
||||
|
||||
it("should configure binary data response", function() {
|
||||
var binaryData = toBinary(toBase64("PDF content here"));
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setFileData(binaryData)
|
||||
.withMime("application/pdf");
|
||||
|
||||
expect(serializer.getFileData()).toBe(binaryData);
|
||||
expect(serializer.getFileMime()).toBe("application/pdf");
|
||||
});
|
||||
|
||||
it("should configure file delete after download", function() {
|
||||
var serializer = new core.nativeJsonSerializer();
|
||||
serializer.setFileName("/tmp/temp-report.pdf")
|
||||
.withMime("application/pdf")
|
||||
.andDelete(true);
|
||||
|
||||
expect(serializer.getDeleteFile()).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function beforeAll() {
|
||||
// Initialize application scope with minimal settings required by resource.cfc
|
||||
application._taffy = {
|
||||
settings: {
|
||||
serializer: "core.nativeJsonSerializer",
|
||||
noDataSends204NoContent: false
|
||||
},
|
||||
factory: new core.factory(),
|
||||
compat: {
|
||||
queryToArray: "missing",
|
||||
queryToStruct: "missing"
|
||||
}
|
||||
};
|
||||
// Create a mock API instance for testing URI methods
|
||||
variables.api = new core.api();
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
describe("URI Matching", function() {
|
||||
|
||||
describe("convertURItoRegex()", function() {
|
||||
|
||||
it("should convert simple static URI to regex", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/users");
|
||||
expect(result).toBeStruct();
|
||||
expect(result).toHaveKey("uriRegex");
|
||||
expect(result).toHaveKey("tokens");
|
||||
expect(result.tokens).toBeArray();
|
||||
expect(arrayLen(result.tokens)).toBe(0);
|
||||
});
|
||||
|
||||
it("should extract single token from URI", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/users/{id}");
|
||||
expect(arrayLen(result.tokens)).toBe(1);
|
||||
expect(result.tokens[1]).toBe("id");
|
||||
});
|
||||
|
||||
it("should extract multiple tokens from URI", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/users/{userId}/orders/{orderId}");
|
||||
expect(arrayLen(result.tokens)).toBe(2);
|
||||
expect(result.tokens[1]).toBe("userId");
|
||||
expect(result.tokens[2]).toBe("orderId");
|
||||
});
|
||||
|
||||
it("should handle mixed static and token segments", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/api/v1/users/{id}/profile");
|
||||
expect(arrayLen(result.tokens)).toBe(1);
|
||||
expect(result.tokens[1]).toBe("id");
|
||||
});
|
||||
|
||||
it("should generate regex that matches expected URIs", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/items/{id}");
|
||||
var regex = result.uriRegex;
|
||||
|
||||
// Should match /items/123
|
||||
expect(reFindNoCase(regex, "/items/123")).toBeGT(0);
|
||||
// Should match /items/abc
|
||||
expect(reFindNoCase(regex, "/items/abc")).toBeGT(0);
|
||||
// Should match with format extension
|
||||
expect(reFindNoCase(regex, "/items/123.json")).toBeGT(0);
|
||||
});
|
||||
|
||||
it("should support custom regex patterns in tokens", function() {
|
||||
// Taffy supports {tokenName:regex} syntax
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/users/{id:[0-9]+}");
|
||||
expect(result.tokens[1]).toBe("id");
|
||||
// The regex should only match numeric IDs
|
||||
var regex = result.uriRegex;
|
||||
expect(reFindNoCase(regex, "/users/123")).toBeGT(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("URI regex matching", function() {
|
||||
|
||||
it("should match exact static path", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/simple");
|
||||
expect(reFindNoCase(result.uriRegex, "/simple")).toBeGT(0);
|
||||
});
|
||||
|
||||
it("should match path with trailing slash", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/simple");
|
||||
expect(reFindNoCase(result.uriRegex, "/simple/")).toBeGT(0);
|
||||
});
|
||||
|
||||
it("should match path with format extension", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/items/{id}");
|
||||
expect(reFindNoCase(result.uriRegex, "/items/123.json")).toBeGT(0);
|
||||
expect(reFindNoCase(result.uriRegex, "/items/abc.xml")).toBeGT(0);
|
||||
});
|
||||
|
||||
it("should not match unrelated paths", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/users/{id}");
|
||||
expect(reFindNoCase(result.uriRegex, "/items/123")).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle deeply nested URIs", function() {
|
||||
var result = makePublic(api, "convertURItoRegex").convertURItoRegex("/api/v1/users/{userId}/orders/{orderId}/items/{itemId}");
|
||||
expect(arrayLen(result.tokens)).toBe(3);
|
||||
expect(reFindNoCase(result.uriRegex, "/api/v1/users/1/orders/2/items/3")).toBeGT(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Token value extraction", function() {
|
||||
|
||||
it("should extract token values from matched URI", function() {
|
||||
var uriConfig = makePublic(api, "convertURItoRegex").convertURItoRegex("/users/{id}");
|
||||
var matchResult = reFindNoSuck(uriConfig.uriRegex, "/users/123");
|
||||
expect(matchResult).toBeArray();
|
||||
expect(arrayLen(matchResult)).toBeGTE(1);
|
||||
expect(matchResult[1]).toBe("123");
|
||||
});
|
||||
|
||||
it("should extract multiple token values", function() {
|
||||
var uriConfig = makePublic(api, "convertURItoRegex").convertURItoRegex("/users/{userId}/orders/{orderId}");
|
||||
var matchResult = reFindNoSuck(uriConfig.uriRegex, "/users/42/orders/99");
|
||||
expect(arrayLen(matchResult)).toBeGTE(2);
|
||||
expect(matchResult[1]).toBe("42");
|
||||
expect(matchResult[2]).toBe("99");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("sortURIMatchOrder()", function() {
|
||||
|
||||
it("should sort URIs for proper matching order", function() {
|
||||
var endpoints = {
|
||||
"^/users/([^\/]+)": { srcUri: "/users/{id}" },
|
||||
"^/users": { srcUri: "/users" },
|
||||
"^/users/([^\/]+)/profile": { srcUri: "/users/{id}/profile" }
|
||||
};
|
||||
var result = makePublic(api, "sortURIMatchOrder").sortURIMatchOrder(endpoints);
|
||||
expect(result).toBeArray();
|
||||
// More specific patterns should be sorted appropriately
|
||||
expect(arrayLen(result)).toBe(3);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reimplementation of reFindNoSuck for testing
|
||||
*/
|
||||
private function reFindNoSuck(required string pattern, required string data, numeric startPos = 1) {
|
||||
var local = {};
|
||||
local.awesome = [];
|
||||
local.sucky = reFindNoCase(arguments.pattern, arguments.data, arguments.startPos, true);
|
||||
if (!isArray(local.sucky.len) || arrayLen(local.sucky.len) == 0) {
|
||||
return [];
|
||||
}
|
||||
for (local.i = 1; local.i <= arrayLen(local.sucky.len); local.i++) {
|
||||
if (local.sucky.len[local.i] > 0 && local.sucky.pos[local.i] > 0) {
|
||||
local.matchBody = mid(arguments.data, local.sucky.pos[local.i], local.sucky.len[local.i]);
|
||||
if (local.matchBody != arguments.data) {
|
||||
arrayAppend(local.awesome, local.matchBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
return local.awesome;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function run() {
|
||||
|
||||
describe("BaseDeserializer", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
variables.deserializer = new core.baseDeserializer();
|
||||
});
|
||||
|
||||
describe("getFromForm()", function() {
|
||||
|
||||
it("should parse simple form data", function() {
|
||||
var body = "name=John&age=30";
|
||||
var result = deserializer.getFromForm(body);
|
||||
expect(result.name).toBe("John");
|
||||
expect(result.age).toBe("30");
|
||||
});
|
||||
|
||||
it("should parse URL-encoded values", function() {
|
||||
var body = "name=John%20Doe&city=New%20York";
|
||||
var result = deserializer.getFromForm(body);
|
||||
expect(result.name).toBe("John Doe");
|
||||
expect(result.city).toBe("New York");
|
||||
});
|
||||
|
||||
it("should handle empty values", function() {
|
||||
var body = "name=John&nickname=";
|
||||
var result = deserializer.getFromForm(body);
|
||||
expect(result.name).toBe("John");
|
||||
expect(result.nickname).toBe("");
|
||||
});
|
||||
|
||||
it("should handle multiple values for same key as list", function() {
|
||||
var body = "color=red&color=blue&color=green";
|
||||
var result = deserializer.getFromForm(body);
|
||||
expect(result.color).toInclude("red");
|
||||
expect(result.color).toInclude("blue");
|
||||
expect(result.color).toInclude("green");
|
||||
});
|
||||
|
||||
it("should handle special characters when URL encoded", function() {
|
||||
var body = "query=" & urlEncodedFormat("foo=bar&baz=qux");
|
||||
var result = deserializer.getFromForm(body);
|
||||
expect(result.query).toBe("foo=bar&baz=qux");
|
||||
});
|
||||
|
||||
it("should handle plus signs as spaces", function() {
|
||||
// Note: urlDecode handles + as space
|
||||
var body = "name=John+Doe";
|
||||
var result = deserializer.getFromForm(body);
|
||||
expect(result.name).toBe("John Doe");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("taffy:mime metadata", function() {
|
||||
|
||||
it("should have taffy:mime for application/x-www-form-urlencoded", function() {
|
||||
var metadata = getMetadata(deserializer);
|
||||
var func = "";
|
||||
for (var f in metadata.functions) {
|
||||
if (f.name == "getFromForm") {
|
||||
func = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(func).notToBeEmpty();
|
||||
var hasMime = structKeyExists(func, "taffy:mime")
|
||||
|| structKeyExists(func, "taffy_mime");
|
||||
expect(hasMime).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function run() {
|
||||
|
||||
describe("BaseSerializer", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
variables.serializer = new core.baseSerializer();
|
||||
});
|
||||
|
||||
describe("setData() and getData()", function() {
|
||||
|
||||
it("should store and retrieve simple string data", function() {
|
||||
var data = "Hello World";
|
||||
serializer.setData(data);
|
||||
expect(serializer.getData()).toBe(data);
|
||||
});
|
||||
|
||||
it("should store and retrieve struct data", function() {
|
||||
var data = { name: "Test", value: 123 };
|
||||
serializer.setData(data);
|
||||
var result = serializer.getData();
|
||||
expect(result.name).toBe("Test");
|
||||
expect(result.value).toBe(123);
|
||||
});
|
||||
|
||||
it("should store and retrieve array data", function() {
|
||||
var data = [1, 2, 3, 4, 5];
|
||||
serializer.setData(data);
|
||||
expect(serializer.getData()).toBeArray();
|
||||
expect(arrayLen(serializer.getData())).toBe(5);
|
||||
});
|
||||
|
||||
it("should return this for method chaining", function() {
|
||||
var result = serializer.setData("test");
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should set type to textual (1) when setting data", function() {
|
||||
serializer.setData("test");
|
||||
expect(serializer.getType()).toBe("textual");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("withStatus()", function() {
|
||||
|
||||
it("should set status code with default text", function() {
|
||||
serializer.withStatus(200);
|
||||
expect(serializer.getStatus()).toBe(200);
|
||||
expect(serializer.getStatusText()).toBe("OK");
|
||||
});
|
||||
|
||||
it("should set status code 201 with Created text", function() {
|
||||
serializer.withStatus(201);
|
||||
expect(serializer.getStatus()).toBe(201);
|
||||
expect(serializer.getStatusText()).toBe("Created");
|
||||
});
|
||||
|
||||
it("should set status code 404 with Not Found text", function() {
|
||||
serializer.withStatus(404);
|
||||
expect(serializer.getStatus()).toBe(404);
|
||||
expect(serializer.getStatusText()).toBe("Not Found");
|
||||
});
|
||||
|
||||
it("should set status code 500 with Internal Server Error text", function() {
|
||||
serializer.withStatus(500);
|
||||
expect(serializer.getStatus()).toBe(500);
|
||||
expect(serializer.getStatusText()).toBe("Internal Server Error");
|
||||
});
|
||||
|
||||
it("should allow custom status text", function() {
|
||||
serializer.withStatus(418, "I'm a teapot - Custom");
|
||||
expect(serializer.getStatus()).toBe(418);
|
||||
expect(serializer.getStatusText()).toBe("I'm a teapot - Custom");
|
||||
});
|
||||
|
||||
it("should use lookup table for known status codes", function() {
|
||||
serializer.withStatus(418);
|
||||
expect(serializer.getStatusText()).toBe("I'm a teapot");
|
||||
});
|
||||
|
||||
it("should return this for method chaining", function() {
|
||||
var result = serializer.withStatus(200);
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should handle all 2xx status codes", function() {
|
||||
serializer.withStatus(204);
|
||||
expect(serializer.getStatusText()).toBe("No Content");
|
||||
|
||||
serializer.withStatus(206);
|
||||
expect(serializer.getStatusText()).toBe("Partial Content");
|
||||
});
|
||||
|
||||
it("should handle 3xx redirect status codes", function() {
|
||||
serializer.withStatus(301);
|
||||
expect(serializer.getStatusText()).toBe("Moved Permanently");
|
||||
|
||||
serializer.withStatus(302);
|
||||
expect(serializer.getStatusText()).toBe("Found");
|
||||
|
||||
serializer.withStatus(304);
|
||||
expect(serializer.getStatusText()).toBe("Not Modified");
|
||||
});
|
||||
|
||||
it("should handle 4xx client error status codes", function() {
|
||||
serializer.withStatus(400);
|
||||
expect(serializer.getStatusText()).toBe("Bad Request");
|
||||
|
||||
serializer.withStatus(401);
|
||||
expect(serializer.getStatusText()).toBe("Unauthorized");
|
||||
|
||||
serializer.withStatus(403);
|
||||
expect(serializer.getStatusText()).toBe("Forbidden");
|
||||
|
||||
serializer.withStatus(405);
|
||||
expect(serializer.getStatusText()).toBe("Method Not Allowed");
|
||||
|
||||
serializer.withStatus(422);
|
||||
expect(serializer.getStatusText()).toBe("Unprocessable Entity");
|
||||
|
||||
serializer.withStatus(429);
|
||||
expect(serializer.getStatusText()).toBe("Too Many Requests");
|
||||
});
|
||||
|
||||
it("should handle 5xx server error status codes", function() {
|
||||
serializer.withStatus(501);
|
||||
expect(serializer.getStatusText()).toBe("Not Implemented");
|
||||
|
||||
serializer.withStatus(502);
|
||||
expect(serializer.getStatusText()).toBe("Bad Gateway");
|
||||
|
||||
serializer.withStatus(503);
|
||||
expect(serializer.getStatusText()).toBe("Service Unavailable");
|
||||
|
||||
serializer.withStatus(504);
|
||||
expect(serializer.getStatusText()).toBe("Gateway Timeout");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("withHeaders()", function() {
|
||||
|
||||
it("should set custom headers", function() {
|
||||
var headers = { "X-Custom-Header": "value1", "X-Another": "value2" };
|
||||
serializer.withHeaders(headers);
|
||||
var result = serializer.getHeaders();
|
||||
expect(result["X-Custom-Header"]).toBe("value1");
|
||||
expect(result["X-Another"]).toBe("value2");
|
||||
});
|
||||
|
||||
it("should return this for method chaining", function() {
|
||||
var result = serializer.withHeaders({});
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return empty struct when no headers set", function() {
|
||||
var result = serializer.getHeaders();
|
||||
expect(result).toBeStruct();
|
||||
expect(structIsEmpty(result)).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("noData()", function() {
|
||||
|
||||
it("should return serializer instance when noDataSends204NoContent is false", function() {
|
||||
serializer.setNoDataSends204NoContent(false);
|
||||
var result = serializer.noData();
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
expect(result.getStatus()).toBe(200);
|
||||
});
|
||||
|
||||
it("should return 204 status when noDataSends204NoContent is true", function() {
|
||||
serializer.setNoDataSends204NoContent(true);
|
||||
var result = serializer.noData();
|
||||
expect(result.getStatus()).toBe(204);
|
||||
});
|
||||
|
||||
it("should default to false (no 204) without explicit configuration", function() {
|
||||
var result = serializer.noData();
|
||||
expect(result.getStatus()).toBe(200);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("noContent()", function() {
|
||||
|
||||
it("should set status to 204 No Content", function() {
|
||||
var result = serializer.noContent();
|
||||
expect(result.getStatus()).toBe(204);
|
||||
expect(result.getStatusText()).toBe("No Content");
|
||||
});
|
||||
|
||||
it("should set Content-Type header to text/plain", function() {
|
||||
var result = serializer.noContent();
|
||||
var headers = result.getHeaders();
|
||||
expect(headers["Content-Type"]).toBe("text/plain");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("File streaming methods", function() {
|
||||
|
||||
describe("setFileName()", function() {
|
||||
|
||||
it("should store filename for streaming", function() {
|
||||
serializer.setFileName("/path/to/file.pdf");
|
||||
expect(serializer.getFileName()).toBe("/path/to/file.pdf");
|
||||
});
|
||||
|
||||
it("should set type to filename (2)", function() {
|
||||
serializer.setFileName("/path/to/file.pdf");
|
||||
expect(serializer.getType()).toBe("filename");
|
||||
});
|
||||
|
||||
it("should return this for method chaining", function() {
|
||||
var result = serializer.setFileName("/path/to/file.pdf");
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("setFileData()", function() {
|
||||
|
||||
it("should store binary data for streaming", function() {
|
||||
var binaryData = toBinary(toBase64("test data"));
|
||||
serializer.setFileData(binaryData);
|
||||
expect(serializer.getFileData()).toBe(binaryData);
|
||||
});
|
||||
|
||||
it("should set type to filedata (3)", function() {
|
||||
serializer.setFileData(toBinary(toBase64("test")));
|
||||
expect(serializer.getType()).toBe("filedata");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("setImageData()", function() {
|
||||
|
||||
it("should store image data for streaming", function() {
|
||||
var imageData = toBinary(toBase64("fake image data"));
|
||||
serializer.setImageData(imageData);
|
||||
expect(serializer.getImageData()).notToBeNull();
|
||||
});
|
||||
|
||||
it("should set type to imagedata (4)", function() {
|
||||
serializer.setImageData(toBinary(toBase64("test")));
|
||||
expect(serializer.getType()).toBe("imagedata");
|
||||
});
|
||||
|
||||
it("should convert non-binary data to binary", function() {
|
||||
serializer.setImageData("test string");
|
||||
expect(isBinary(serializer.getImageData())).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("withMime()", function() {
|
||||
|
||||
it("should set mime type for file streaming", function() {
|
||||
serializer.withMime("application/pdf");
|
||||
expect(serializer.getFileMime()).toBe("application/pdf");
|
||||
});
|
||||
|
||||
it("should return this for method chaining", function() {
|
||||
var result = serializer.withMime("image/jpeg");
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("andDelete()", function() {
|
||||
|
||||
it("should set delete flag to true", function() {
|
||||
serializer.andDelete(true);
|
||||
expect(serializer.getDeleteFile()).toBeTrue();
|
||||
});
|
||||
|
||||
it("should set delete flag to false", function() {
|
||||
serializer.andDelete(false);
|
||||
expect(serializer.getDeleteFile()).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return this for method chaining", function() {
|
||||
var result = serializer.andDelete(true);
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Method chaining", function() {
|
||||
|
||||
it("should support fluent interface for multiple operations", function() {
|
||||
var result = serializer
|
||||
.setData({ message: "test" })
|
||||
.withStatus(201, "Created")
|
||||
.withHeaders({ "X-Custom": "value" });
|
||||
|
||||
expect(result.getData().message).toBe("test");
|
||||
expect(result.getStatus()).toBe(201);
|
||||
expect(result.getHeaders()["X-Custom"]).toBe("value");
|
||||
});
|
||||
|
||||
it("should support chaining for file streaming", function() {
|
||||
var result = serializer
|
||||
.setFileName("/path/to/file.pdf")
|
||||
.withMime("application/pdf")
|
||||
.andDelete(true);
|
||||
|
||||
expect(result.getFileName()).toBe("/path/to/file.pdf");
|
||||
expect(result.getFileMime()).toBe("application/pdf");
|
||||
expect(result.getDeleteFile()).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function run() {
|
||||
|
||||
describe("Factory", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
variables.factory = new core.factory();
|
||||
});
|
||||
|
||||
describe("init()", function() {
|
||||
|
||||
it("should initialize with empty bean cache", function() {
|
||||
expect(factory.beans).toBeStruct();
|
||||
expect(structIsEmpty(factory.beans)).toBeTrue();
|
||||
});
|
||||
|
||||
it("should initialize with empty transients cache", function() {
|
||||
expect(factory.transients).toBeStruct();
|
||||
expect(structIsEmpty(factory.transients)).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return this for method chaining", function() {
|
||||
var result = factory.init();
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "factory")).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("beanExists() / containsBean()", function() {
|
||||
|
||||
it("should return false for non-existent bean", function() {
|
||||
expect(factory.beanExists("nonExistent")).toBeFalse();
|
||||
expect(factory.containsBean("nonExistent")).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return true for cached bean", function() {
|
||||
factory.beans["testBean"] = { name: "test" };
|
||||
expect(factory.beanExists("testBean")).toBeTrue();
|
||||
expect(factory.containsBean("testBean")).toBeTrue();
|
||||
});
|
||||
|
||||
it("containsBean should be alias for beanExists", function() {
|
||||
factory.beans["aliasTest"] = { name: "test" };
|
||||
expect(factory.containsBean("aliasTest")).toBe(factory.beanExists("aliasTest"));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("transientExists()", function() {
|
||||
|
||||
it("should return false for non-existent transient", function() {
|
||||
expect(factory.transientExists("nonExistent")).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return true for cached transient", function() {
|
||||
factory.transients["testTransient"] = "some.component.path";
|
||||
expect(factory.transientExists("testTransient")).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("getBean()", function() {
|
||||
|
||||
it("should return cached bean if exists", function() {
|
||||
var testObj = { name: "cached bean" };
|
||||
factory.beans["testBean"] = testObj;
|
||||
var result = factory.getBean("testBean");
|
||||
expect(result.name).toBe("cached bean");
|
||||
});
|
||||
|
||||
it("should throw error for non-existent bean", function() {
|
||||
expect(function() {
|
||||
factory.getBean("nonExistent");
|
||||
}).toThrow("Taffy.Factory.BeanNotFound");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("getBeanList()", function() {
|
||||
|
||||
it("should return empty string when no beans", function() {
|
||||
expect(factory.getBeanList()).toBe("");
|
||||
});
|
||||
|
||||
it("should return bean names as comma-separated list", function() {
|
||||
factory.beans["bean1"] = {};
|
||||
factory.beans["bean2"] = {};
|
||||
var list = factory.getBeanList();
|
||||
expect(listLen(list)).toBe(2);
|
||||
expect(listFindNoCase(list, "bean1")).toBeGT(0);
|
||||
expect(listFindNoCase(list, "bean2")).toBeGT(0);
|
||||
});
|
||||
|
||||
it("should include transients in the list", function() {
|
||||
factory.beans["bean1"] = {};
|
||||
factory.transients["transient1"] = "some.path";
|
||||
var list = factory.getBeanList();
|
||||
expect(listLen(list)).toBe(2);
|
||||
expect(listFindNoCase(list, "transient1")).toBeGT(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("loadBeansFromPath()", function() {
|
||||
|
||||
it("should not throw error for non-existent path", function() {
|
||||
// loadBeansFromPath should handle non-existent directories gracefully
|
||||
expect(function() {
|
||||
factory.loadBeansFromPath("/non/existent/path");
|
||||
}).notToThrow();
|
||||
});
|
||||
|
||||
it("should clear beans on full reload", function() {
|
||||
factory.beans["existingBean"] = {};
|
||||
var taffyRef = {
|
||||
status: { skippedResources: [] },
|
||||
beanList: ""
|
||||
};
|
||||
factory.loadBeansFromPath(
|
||||
beanPath = "/non/existent/path",
|
||||
resourcesPath = "resources",
|
||||
resourcesBasePath = "",
|
||||
isFullReload = true,
|
||||
taffyRef = taffyRef
|
||||
);
|
||||
// After reload with non-existent path, beans should be cleared
|
||||
expect(structIsEmpty(factory.beans)).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("External bean factory integration", function() {
|
||||
|
||||
it("should accept external bean factory in init", function() {
|
||||
var mockExternalFactory = {
|
||||
containsBean: function(beanName) { return beanName == "externalBean"; },
|
||||
getBean: function(beanName) { return { name: "external" }; }
|
||||
};
|
||||
factory.init(mockExternalFactory);
|
||||
expect(structKeyExists(factory, "externalBeanFactory")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should check external factory for beans when configured", function() {
|
||||
var mockExternalFactory = {
|
||||
containsBean: function(beanName) { return beanName == "externalBean"; },
|
||||
getBean: function(beanName) { return { name: "from external", source: "external" }; }
|
||||
};
|
||||
factory.init(mockExternalFactory);
|
||||
|
||||
// beanExists with includeExternal=true should find external beans
|
||||
expect(factory.beanExists("externalBean", true, true)).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function run() {
|
||||
|
||||
describe("NativeJsonDeserializer", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
variables.deserializer = new core.nativeJsonDeserializer();
|
||||
});
|
||||
|
||||
describe("inheritance", function() {
|
||||
|
||||
it("should extend baseDeserializer", function() {
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(deserializer, "baseDeserializer")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should have getFromForm from parent", function() {
|
||||
expect(deserializer).toHaveKey("getFromForm");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("getFromJson()", function() {
|
||||
|
||||
it("should parse simple JSON object", function() {
|
||||
var body = '{"name":"John","age":30}';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(result.name).toBe("John");
|
||||
expect(result.age).toBe(30);
|
||||
});
|
||||
|
||||
it("should parse nested JSON objects", function() {
|
||||
var body = '{"user":{"name":"John","email":"john@example.com"},"active":true}';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(result.user.name).toBe("John");
|
||||
expect(result.user.email).toBe("john@example.com");
|
||||
expect(result.active).toBeTrue();
|
||||
});
|
||||
|
||||
it("should parse JSON with arrays", function() {
|
||||
var body = '{"items":[1,2,3],"tags":["a","b","c"]}';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(result.items).toBeArray();
|
||||
expect(arrayLen(result.items)).toBe(3);
|
||||
expect(result.tags[1]).toBe("a");
|
||||
});
|
||||
|
||||
it("should handle boolean values", function() {
|
||||
var body = '{"active":true,"deleted":false}';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(result.active).toBeTrue();
|
||||
expect(result.deleted).toBeFalse();
|
||||
});
|
||||
|
||||
it("should handle null values", function() {
|
||||
var body = '{"value":null,"other":"test"}';
|
||||
var result = deserializer.getFromJson(body);
|
||||
// Note: In Lucee, null values may not preserve the key in the struct
|
||||
// We verify the deserializer doesn't throw an error and returns a struct
|
||||
expect(result).toBeStruct();
|
||||
expect(result.other).toBe("test");
|
||||
});
|
||||
|
||||
it("should handle numeric values", function() {
|
||||
var body = '{"integer":42,"decimal":3.14,"negative":-100}';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(result.integer).toBe(42);
|
||||
expect(result.decimal).toBe(3.14);
|
||||
expect(result.negative).toBe(-100);
|
||||
});
|
||||
|
||||
it("should handle empty object", function() {
|
||||
var body = '{}';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(result).toBeStruct();
|
||||
expect(structIsEmpty(result)).toBeTrue();
|
||||
});
|
||||
|
||||
it("should wrap non-struct JSON in _body key", function() {
|
||||
// When JSON is an array at root level, wrap it
|
||||
var body = '[1,2,3]';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(structKeyExists(result, "_body")).toBeTrue();
|
||||
expect(result._body).toBeArray();
|
||||
});
|
||||
|
||||
it("should wrap primitive JSON values in _body key", function() {
|
||||
var body = '"just a string"';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(structKeyExists(result, "_body")).toBeTrue();
|
||||
expect(result._body).toBe("just a string");
|
||||
});
|
||||
|
||||
it("should handle complex nested structures", function() {
|
||||
var body = '{
|
||||
"users": [
|
||||
{"id": 1, "name": "John", "roles": ["admin", "user"]},
|
||||
{"id": 2, "name": "Jane", "roles": ["user"]}
|
||||
],
|
||||
"metadata": {
|
||||
"total": 2,
|
||||
"page": 1
|
||||
}
|
||||
}';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(result.users).toBeArray();
|
||||
expect(arrayLen(result.users)).toBe(2);
|
||||
expect(result.users[1].roles).toBeArray();
|
||||
expect(result.metadata.total).toBe(2);
|
||||
});
|
||||
|
||||
it("should handle unicode characters", function() {
|
||||
var body = '{"message":"Hello \u4e16\u754c"}';
|
||||
var result = deserializer.getFromJson(body);
|
||||
expect(structKeyExists(result, "message")).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("taffy:mime metadata", function() {
|
||||
|
||||
it("should have taffy:mime for application/json", function() {
|
||||
var metadata = getMetadata(deserializer);
|
||||
var func = "";
|
||||
for (var f in metadata.functions) {
|
||||
if (f.name == "getFromJson") {
|
||||
func = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(func).notToBeEmpty();
|
||||
var hasMime = structKeyExists(func, "taffy:mime")
|
||||
|| structKeyExists(func, "taffy_mime");
|
||||
expect(hasMime).toBeTrue();
|
||||
|
||||
// Check the actual mime types
|
||||
var mimeValue = structKeyExists(func, "taffy:mime")
|
||||
? func["taffy:mime"]
|
||||
: func["taffy_mime"];
|
||||
expect(mimeValue).toInclude("application/json");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function beforeAll() {
|
||||
// Initialize application scope with minimal settings
|
||||
application._taffy = {
|
||||
settings: {
|
||||
serializer: "core.nativeJsonSerializer",
|
||||
noDataSends204NoContent: false
|
||||
},
|
||||
factory: new core.factory(),
|
||||
compat: {
|
||||
queryToArray: "missing",
|
||||
queryToStruct: "missing"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
describe("NativeJsonSerializer", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
variables.serializer = new core.nativeJsonSerializer();
|
||||
});
|
||||
|
||||
describe("inheritance", function() {
|
||||
|
||||
it("should extend baseSerializer", function() {
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(serializer, "baseSerializer")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should have all baseSerializer methods", function() {
|
||||
expect(serializer).toHaveKey("setData");
|
||||
expect(serializer).toHaveKey("getData");
|
||||
expect(serializer).toHaveKey("withStatus");
|
||||
expect(serializer).toHaveKey("withHeaders");
|
||||
expect(serializer).toHaveKey("noData");
|
||||
expect(serializer).toHaveKey("noContent");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("getAsJson()", function() {
|
||||
|
||||
it("should serialize simple string to JSON", function() {
|
||||
serializer.setData("Hello World");
|
||||
var result = serializer.getAsJson();
|
||||
expect(result).toBe('"Hello World"');
|
||||
});
|
||||
|
||||
it("should serialize struct to JSON", function() {
|
||||
serializer.setData({ name: "Test", value: 123 });
|
||||
var result = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(result);
|
||||
expect(parsed.name).toBe("Test");
|
||||
expect(parsed.value).toBe(123);
|
||||
});
|
||||
|
||||
it("should serialize array to JSON", function() {
|
||||
serializer.setData([1, 2, 3]);
|
||||
var result = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(result);
|
||||
expect(parsed).toBeArray();
|
||||
expect(arrayLen(parsed)).toBe(3);
|
||||
});
|
||||
|
||||
it("should serialize nested structures to JSON", function() {
|
||||
serializer.setData({
|
||||
user: {
|
||||
name: "John",
|
||||
roles: ["admin", "user"]
|
||||
},
|
||||
active: true
|
||||
});
|
||||
var result = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(result);
|
||||
expect(parsed.user.name).toBe("John");
|
||||
expect(parsed.user.roles).toBeArray();
|
||||
expect(parsed.active).toBeTrue();
|
||||
});
|
||||
|
||||
it("should serialize boolean values correctly", function() {
|
||||
serializer.setData({ active: true, deleted: false });
|
||||
var result = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(result);
|
||||
expect(parsed.active).toBeTrue();
|
||||
expect(parsed.deleted).toBeFalse();
|
||||
});
|
||||
|
||||
it("should serialize null values", function() {
|
||||
serializer.setData({ value: javacast("null", "") });
|
||||
var result = serializer.getAsJson();
|
||||
expect(result).toInclude("null");
|
||||
});
|
||||
|
||||
it("should handle empty struct", function() {
|
||||
serializer.setData({});
|
||||
var result = serializer.getAsJson();
|
||||
expect(result).toBe("{}");
|
||||
});
|
||||
|
||||
it("should handle empty array", function() {
|
||||
serializer.setData([]);
|
||||
var result = serializer.getAsJson();
|
||||
expect(result).toBe("[]");
|
||||
});
|
||||
|
||||
it("should handle numeric values", function() {
|
||||
serializer.setData({
|
||||
integer: 42,
|
||||
decimal: 3.14,
|
||||
negative: -100
|
||||
});
|
||||
var result = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(result);
|
||||
expect(parsed.integer).toBe(42);
|
||||
expect(parsed.decimal).toBe(3.14);
|
||||
expect(parsed.negative).toBe(-100);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("taffy:mime metadata", function() {
|
||||
|
||||
it("should have taffy:mime metadata for application/json", function() {
|
||||
var metadata = getMetadata(serializer);
|
||||
var getAsJsonFunc = "";
|
||||
for (var func in metadata.functions) {
|
||||
if (func.name == "getAsJson") {
|
||||
getAsJsonFunc = func;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(getAsJsonFunc).notToBeEmpty();
|
||||
var hasMime = structKeyExists(getAsJsonFunc, "taffy:mime")
|
||||
|| structKeyExists(getAsJsonFunc, "taffy_mime");
|
||||
expect(hasMime).toBeTrue();
|
||||
});
|
||||
|
||||
it("should be marked as default serializer", function() {
|
||||
var metadata = getMetadata(serializer);
|
||||
var getAsJsonFunc = "";
|
||||
for (var func in metadata.functions) {
|
||||
if (func.name == "getAsJson") {
|
||||
getAsJsonFunc = func;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var hasDefault = structKeyExists(getAsJsonFunc, "taffy:default")
|
||||
|| structKeyExists(getAsJsonFunc, "taffy_default");
|
||||
expect(hasDefault).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("encode.string helper", function() {
|
||||
|
||||
it("should force numeric strings to serialize as strings", function() {
|
||||
// The forceString function adds chr(2) which is then stripped by getAsJson
|
||||
// This ensures numeric-looking strings serialize as strings, not numbers
|
||||
var resource = new core.resource();
|
||||
var forcedString = resource.forceString("12345");
|
||||
|
||||
// The chr(2) prefix forces CF to treat this as a string during serialization
|
||||
expect(left(forcedString, 1)).toBe(chr(2));
|
||||
|
||||
// When serialized via nativeJsonSerializer, the chr(2) is stripped
|
||||
serializer.setData({ value: forcedString });
|
||||
var result = serializer.getAsJson();
|
||||
// The value should be serialized as a string (with quotes), not a number
|
||||
expect(result).toInclude('"');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("combined operations", function() {
|
||||
|
||||
it("should work with setData and status code", function() {
|
||||
serializer.setData({ error: "Not found" }).withStatus(404);
|
||||
var result = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(result);
|
||||
expect(parsed.error).toBe("Not found");
|
||||
expect(serializer.getStatus()).toBe(404);
|
||||
});
|
||||
|
||||
it("should work with setData, status, and headers", function() {
|
||||
serializer
|
||||
.setData({ created: true })
|
||||
.withStatus(201)
|
||||
.withHeaders({ "Location": "/items/123" });
|
||||
|
||||
var result = serializer.getAsJson();
|
||||
var parsed = deserializeJSON(result);
|
||||
expect(parsed.created).toBeTrue();
|
||||
expect(serializer.getStatus()).toBe(201);
|
||||
expect(serializer.getHeaders()["Location"]).toBe("/items/123");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function beforeAll() {
|
||||
variables.gen = new core.openapi();
|
||||
makePublic(variables.gen, "normalizePathTemplate");
|
||||
makePublic(variables.gen, "cfmlTypeToSchema");
|
||||
makePublic(variables.gen, "buildOperation");
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
describe("openapi generator", function() {
|
||||
|
||||
describe("normalizePathTemplate", function() {
|
||||
|
||||
it("leaves simple paths untouched", function() {
|
||||
expect(gen.normalizePathTemplate("/users", [])).toBe("/users");
|
||||
});
|
||||
|
||||
it("leaves {name} tokens untouched", function() {
|
||||
expect(gen.normalizePathTemplate("/users/{id}", ["id"])).toBe("/users/{id}");
|
||||
});
|
||||
|
||||
it("strips {name:regex} down to {name}", function() {
|
||||
expect(gen.normalizePathTemplate("/users/{id:[0-9]+}", ["id"])).toBe("/users/{id}");
|
||||
});
|
||||
|
||||
it("strips greedy regex like {name:.+}", function() {
|
||||
expect(gen.normalizePathTemplate("/files/{path:.+}", ["path"])).toBe("/files/{path}");
|
||||
});
|
||||
|
||||
it("handles nested braces in token regexes", function() {
|
||||
expect(gen.normalizePathTemplate("/a/{b:[a-z]+(?:42){1}}", ["b"])).toBe("/a/{b}");
|
||||
});
|
||||
|
||||
it("rewrites multiple tokens in order", function() {
|
||||
expect(gen.normalizePathTemplate("/events/{eventId:\d+}/attendees/{personId}", ["eventId", "personId"]))
|
||||
.toBe("/events/{eventId}/attendees/{personId}");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("cfmlTypeToSchema", function() {
|
||||
|
||||
it("maps numeric to number", function() {
|
||||
expect(gen.cfmlTypeToSchema("numeric")).toBe({ "type": "number" });
|
||||
});
|
||||
|
||||
it("maps boolean to boolean", function() {
|
||||
expect(gen.cfmlTypeToSchema("boolean")).toBe({ "type": "boolean" });
|
||||
});
|
||||
|
||||
it("maps date to string/date-time", function() {
|
||||
expect(gen.cfmlTypeToSchema("date")).toBe({ "type": "string", "format": "date-time" });
|
||||
});
|
||||
|
||||
it("maps uuid to string/uuid", function() {
|
||||
expect(gen.cfmlTypeToSchema("uuid")).toBe({ "type": "string", "format": "uuid" });
|
||||
});
|
||||
|
||||
it("returns empty schema for 'any'", function() {
|
||||
expect(gen.cfmlTypeToSchema("any")).toBe({});
|
||||
});
|
||||
|
||||
it("returns empty schema for unset type", function() {
|
||||
expect(gen.cfmlTypeToSchema("")).toBe({});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("buildOperation — path param handling", function() {
|
||||
|
||||
// minimal fixtures
|
||||
var mkEndpoint = function(beanName, srcUri, tokens, methods) {
|
||||
return { beanName: beanName, srcUri: srcUri, tokens: tokens, methods: methods };
|
||||
};
|
||||
var mkFunc = function(name, parameters = []) {
|
||||
return { name: name, parameters: parameters };
|
||||
};
|
||||
|
||||
it("backfills path tokens not declared as function args", function() {
|
||||
var endpoint = mkEndpoint("MyResource", "/items/{id}", ["id"], { get: "get" });
|
||||
var func = mkFunc("get", []); // function declares NO args
|
||||
var op = gen.buildOperation("get", func, endpoint, {}, ["application/json"]);
|
||||
|
||||
expect(op).toHaveKey("parameters");
|
||||
expect(arrayLen(op.parameters)).toBe(1);
|
||||
expect(op.parameters[1].name).toBe("id");
|
||||
expect(op.parameters[1]["in"]).toBe("path");
|
||||
expect(op.parameters[1].required).toBeTrue();
|
||||
});
|
||||
|
||||
it("uses token's original case when arg casing differs", function() {
|
||||
var endpoint = mkEndpoint("AttendeesResource", "/attendees/{personId}", ["personId"], { put: "put" });
|
||||
// simulate CFML metadata that lowercased the arg name
|
||||
var func = mkFunc("put", [ { name: "personid", type: "numeric", required: true } ]);
|
||||
var op = gen.buildOperation("put", func, endpoint, {}, ["application/json"]);
|
||||
|
||||
expect(op.parameters[1].name).toBe("personId");
|
||||
expect(op.parameters[1]["in"]).toBe("path");
|
||||
expect(op.parameters[1].schema).toBe({ "type": "number" });
|
||||
});
|
||||
|
||||
it("puts non-token args for GET into query", function() {
|
||||
var endpoint = mkEndpoint("ItemsResource", "/items", [], { get: "get" });
|
||||
var func = mkFunc("get", [ { name: "limit", type: "numeric", required: false } ]);
|
||||
var op = gen.buildOperation("get", func, endpoint, {}, ["application/json"]);
|
||||
|
||||
expect(op.parameters[1].name).toBe("limit");
|
||||
expect(op.parameters[1]["in"]).toBe("query");
|
||||
expect(op.parameters[1].required).toBeFalse();
|
||||
});
|
||||
|
||||
it("puts non-token args for POST into requestBody with both json + form", function() {
|
||||
var endpoint = mkEndpoint("ItemsResource", "/items", [], { post: "post" });
|
||||
var func = mkFunc("post", [ { name: "name", type: "string", required: true } ]);
|
||||
var op = gen.buildOperation("post", func, endpoint, {}, ["application/json"]);
|
||||
|
||||
expect(op).toHaveKey("requestBody");
|
||||
expect(op.requestBody.content).toHaveKey("application/json");
|
||||
expect(op.requestBody.content).toHaveKey("application/x-www-form-urlencoded");
|
||||
expect(op.requestBody.content["application/json"].schema.properties).toHaveKey("name");
|
||||
expect(op.requestBody.content["application/json"].schema.required).toBe(["name"]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
component extends="testbox.system.BaseSpec" {
|
||||
|
||||
function beforeAll() {
|
||||
// Initialize application scope with minimal settings
|
||||
application._taffy = {
|
||||
settings: {
|
||||
serializer: "core.nativeJsonSerializer",
|
||||
noDataSends204NoContent: false
|
||||
},
|
||||
factory: new core.factory(),
|
||||
compat: {
|
||||
queryToArray: "missing",
|
||||
queryToStruct: "missing"
|
||||
}
|
||||
};
|
||||
|
||||
// Create sample data fixture
|
||||
variables.sampleData = new tests.fixtures.SampleData();
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
describe("Resource", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
variables.resource = new core.resource();
|
||||
});
|
||||
|
||||
describe("rep() / representationOf()", function() {
|
||||
|
||||
it("should return a serializer instance with data", function() {
|
||||
var result = resource.rep({ message: "test" });
|
||||
expect(new tests.helpers.MetadataHelper().extendsClass(result, "baseSerializer")).toBeTrue();
|
||||
expect(result.getData().message).toBe("test");
|
||||
});
|
||||
|
||||
it("rep() should be alias for representationOf()", function() {
|
||||
var data = { id: 123 };
|
||||
var rep1 = resource.rep(data);
|
||||
// Both should return serializer instances with same data
|
||||
expect(rep1.getData().id).toBe(123);
|
||||
});
|
||||
|
||||
it("should work with simple string data", function() {
|
||||
var result = resource.rep("Hello World");
|
||||
expect(result.getData()).toBe("Hello World");
|
||||
});
|
||||
|
||||
it("should work with array data", function() {
|
||||
var result = resource.rep([1, 2, 3]);
|
||||
expect(result.getData()).toBeArray();
|
||||
expect(arrayLen(result.getData())).toBe(3);
|
||||
});
|
||||
|
||||
it("should work with nested struct data", function() {
|
||||
var data = {
|
||||
user: { name: "John", age: 30 },
|
||||
roles: ["admin", "user"]
|
||||
};
|
||||
var result = resource.rep(data);
|
||||
expect(result.getData().user.name).toBe("John");
|
||||
expect(result.getData().roles).toBeArray();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("encode.string()", function() {
|
||||
|
||||
it("should add control character prefix to force string serialization", function() {
|
||||
// forceString is public (no access modifier), encode.string is just a reference to it
|
||||
var result = resource.forceString("12345");
|
||||
expect(left(result, 1)).toBe(chr(2));
|
||||
});
|
||||
|
||||
it("should preserve the original value after prefix", function() {
|
||||
var result = resource.forceString("12345");
|
||||
expect(right(result, 5)).toBe("12345");
|
||||
});
|
||||
|
||||
it("should work with numeric-looking strings", function() {
|
||||
var result = resource.forceString("00123");
|
||||
expect(len(result)).toBe(6); // chr(2) + "00123"
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("qToArray() - internal query to array conversion", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
makePublic(resource, "qToArray");
|
||||
});
|
||||
|
||||
it("should convert query to array of structs", function() {
|
||||
var q = sampleData.getSampleQuery();
|
||||
var result = resource.qToArray(q);
|
||||
expect(result).toBeArray();
|
||||
expect(arrayLen(result)).toBe(3);
|
||||
});
|
||||
|
||||
it("should preserve column values in structs", function() {
|
||||
var q = sampleData.getSampleQuery();
|
||||
var result = resource.qToArray(q);
|
||||
// Find the row with id=1
|
||||
var found = false;
|
||||
for (var row in result) {
|
||||
if (row.id == 1) {
|
||||
expect(row.name).toBe("John Doe");
|
||||
expect(row.email).toBe("john@example.com");
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(found).toBeTrue();
|
||||
});
|
||||
|
||||
it("should handle empty query", function() {
|
||||
var q = queryNew("id,name", "integer,varchar");
|
||||
var result = resource.qToArray(q);
|
||||
expect(result).toBeArray();
|
||||
expect(arrayLen(result)).toBe(0);
|
||||
});
|
||||
|
||||
it("should support callback function for row transformation", function() {
|
||||
var q = sampleData.getSingleRowQuery();
|
||||
var result = resource.qToArray(q, function(row) {
|
||||
row.transformed = true;
|
||||
return row;
|
||||
});
|
||||
expect(result[1].transformed).toBeTrue();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("qToStruct() - internal query to struct conversion", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
makePublic(resource, "qToStruct");
|
||||
});
|
||||
|
||||
it("should convert single-row query to struct", function() {
|
||||
var q = sampleData.getSingleRowQuery();
|
||||
var result = resource.qToStruct(q);
|
||||
expect(result).toBeStruct();
|
||||
expect(result.id).toBe(1);
|
||||
expect(result.name).toBe("Test User");
|
||||
});
|
||||
|
||||
it("should throw error for multi-row query", function() {
|
||||
var q = sampleData.getSampleQuery(); // Has 3 rows
|
||||
expect(function() {
|
||||
resource.qToStruct(q);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should handle query with all column types", function() {
|
||||
var q = sampleData.getSingleRowQuery();
|
||||
var result = resource.qToStruct(q);
|
||||
expect(structKeyExists(result, "id")).toBeTrue();
|
||||
expect(structKeyExists(result, "name")).toBeTrue();
|
||||
expect(structKeyExists(result, "email")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should support callback function for value transformation", function() {
|
||||
var q = sampleData.getSingleRowQuery();
|
||||
var result = resource.qToStruct(q, function(colName, value) {
|
||||
if (colName == "name") {
|
||||
return uCase(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
expect(result.name).toBe("TEST USER");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("method chaining with rep()", function() {
|
||||
|
||||
it("should support chaining with withStatus()", function() {
|
||||
var result = resource.rep({ created: true }).withStatus(201);
|
||||
expect(result.getStatus()).toBe(201);
|
||||
expect(result.getData().created).toBeTrue();
|
||||
});
|
||||
|
||||
it("should support chaining with withHeaders()", function() {
|
||||
var result = resource.rep({ data: "test" })
|
||||
.withHeaders({ "X-Custom": "value" });
|
||||
expect(result.getHeaders()["X-Custom"]).toBe("value");
|
||||
});
|
||||
|
||||
it("should support full fluent chain", function() {
|
||||
var result = resource.rep({ id: 123 })
|
||||
.withStatus(201, "Created")
|
||||
.withHeaders({ "Location": "/items/123" });
|
||||
|
||||
expect(result.getData().id).toBe(123);
|
||||
expect(result.getStatus()).toBe(201);
|
||||
expect(result.getStatusText()).toBe("Created");
|
||||
expect(result.getHeaders()["Location"]).toBe("/items/123");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
component extends="taffy.core.api" {
|
||||
|
||||
this.name = "TaffyE2ETestAPI_" & hash(getCurrentTemplatePath());
|
||||
this.sessionManagement = false;
|
||||
|
||||
variables.testApiPath = getDirectoryFromPath(getCurrentTemplatePath());
|
||||
variables.rootPath = getDirectoryFromPath(left(variables.testApiPath, len(variables.testApiPath) - 1));
|
||||
variables.rootPath = getDirectoryFromPath(left(variables.rootPath, len(variables.rootPath) - 1));
|
||||
|
||||
this.mappings["/taffy"] = variables.rootPath;
|
||||
this.mappings["/resources"] = variables.testApiPath & "resources";
|
||||
|
||||
variables.framework = {
|
||||
disableDashboard: true,
|
||||
disabledDashboardRedirect: "",
|
||||
returnExceptionsAsJson: true
|
||||
};
|
||||
|
||||
function onApplicationStart() {
|
||||
return super.onApplicationStart();
|
||||
}
|
||||
|
||||
function onRequestStart(targetPath) {
|
||||
return super.onRequestStart(targetPath);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<!--- This file intentionally left empty. It is required by Taffy to serve as the entry point. --->
|
||||
@@ -0,0 +1,17 @@
|
||||
component extends="taffy.core.resource" taffy:uri="/echo/{id}" output="false" {
|
||||
|
||||
function get(required string id) output="false" {
|
||||
return rep({ method: "GET", id: arguments.id });
|
||||
}
|
||||
|
||||
function put(required string id, string name = "") output="false" {
|
||||
return rep({ method: "PUT", id: arguments.id, name: arguments.name });
|
||||
}
|
||||
|
||||
function delete(required string id) output="false" {
|
||||
return rep({ method: "DELETE", id: arguments.id })
|
||||
.withStatus(200)
|
||||
.withHeaders({ "X-Deleted-Id": arguments.id });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
component extends="taffy.core.resource" taffy:uri="/echo/{parentId}/child/{childId}" output="false" {
|
||||
|
||||
function get(required string parentId, required string childId) output="false" {
|
||||
return rep({ method: "GET", parentId: arguments.parentId, childId: arguments.childId });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
component extends="taffy.core.resource" taffy:uri="/echo" output="false" {
|
||||
|
||||
function get() output="false" {
|
||||
return rep({ method: "GET", message: "echo" });
|
||||
}
|
||||
|
||||
function post(string name = "" taffy_minlength="1" taffy_maxlength="255", string value = "" taffy_maxlength="500") output="false" {
|
||||
return rep({ method: "POST", name: arguments.name, value: arguments.value })
|
||||
.withStatus(201);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<cfcomponent>
|
||||
|
||||
<cfset this.name = 'Taffy_testSuite' /> <!---uses the same application name as the api Application.cfc for scope sharing --->
|
||||
|
||||
<cfset this.appDirectory = getDirectoryFromPath(getCurrentTemplatePath())>
|
||||
<cfset this.parentDirectory = reReplace(this.appDirectory, "(.+[/\\])tests[/\\]$", "\1")>
|
||||
<cfset this.mappings = structNew()>
|
||||
<cfset this.mappings["/mxunit"] = this.parentDirectory & "testbox/system/compat/">
|
||||
<cfset this.mappings["/testbox"] = this.parentDirectory & "testbox/">
|
||||
<cfset this.mappings["/Hoth"] = this.parentDirectory & "Hoth/">
|
||||
<cfset this.mappings["/di1"] = this.parentDirectory & "di1/">
|
||||
<cfset this.mappings["/bugLog"] = this.parentDirectory & "BugLogHQ/">
|
||||
|
||||
<cfscript>
|
||||
//remove bugLogHQ Application.cfc so we can override datasource definition
|
||||
if (fileExists("#this.parentDirectory#/BugLogHQ/Application.cfc")) {
|
||||
fileDelete("#this.parentDirectory#/BugLogHQ/Application.cfc");
|
||||
}
|
||||
</cfscript>
|
||||
|
||||
<cffunction name="onRequestStart" returnType="void" access="public" output="false">
|
||||
<cfif NOT isDefined('application._taffy')>
|
||||
<cfset local.apiRootURL = getDirectoryFromPath(cgi.script_name) />
|
||||
<cfset local.apiRootURL = listDeleteAt(local.apiRootURL,listLen(local.apiRootURL,'/'),'/') />
|
||||
<cfhttp method="GET" url="http://#CGI.SERVER_NAME#:#CGI.SERVER_PORT##local.apiRootURL#/index.cfm" />
|
||||
</cfif>
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,207 +0,0 @@
|
||||
<cfcomponent hint="automatic test suite runner">
|
||||
|
||||
<cfset this.s_results = StructNew()>
|
||||
<cfset this.s_results.Errors = StructNew()>
|
||||
<cfset sep = createObject("java","java.lang.System").getProperty("file.separator")>
|
||||
|
||||
<cffunction name="run" access="public" hint="runs a directory of tests" returntype="any" output="false">
|
||||
<cfargument name="directory" required="true" hint="directory of tests to run">
|
||||
<cfargument name="componentPath" required="false" hint="the component path to put in front of all tests found (i.e. 'com.blah'). If no path is passed, we'll attempt to discover it ourselves" default="">
|
||||
<cfargument name="recurse" required="false" type="boolean" default="true" hint="whether to recurse down the directory tree">
|
||||
<cfargument name="excludes" required="false" default="" hint="list of Tests, in cfc notation, to exclude. uses ListContains so it's as greedy as possible. Currently does not support ant-style syntax or whole-directory filtering">
|
||||
<cfargument name="refreshCache" required="false" default="false" hint="flag to indicate whether or not to refresh the CF cache of CFCs. Maybe needed if any of the tests in the directory have not yet been compiled - to do: prove" />
|
||||
|
||||
<cfset var testResult = "">
|
||||
<cfset var files = "">
|
||||
<cfset var suite = createObject("component","mxunit.framework.TestSuite")>
|
||||
<cfset var i = 1>
|
||||
|
||||
<cfset arguments.directory = normalizeDirectory(arguments.directory)>
|
||||
|
||||
<cfif NOT len(arguments.componentPath)>
|
||||
<cfset arguments.componentPath = getComponentPath(arguments.directory, arguments.refreshCache)>
|
||||
</cfif>
|
||||
|
||||
<cfset files = getTests(arguments.directory,arguments.componentPath,arguments.recurse,trim(arguments.excludes)) />
|
||||
|
||||
<cfloop from="1" to="#ArrayLen(files)#" index="i">
|
||||
<cftry>
|
||||
<cfset suite.addAll(files[i])>
|
||||
|
||||
<cfcatch>
|
||||
<cfset this.s_results.Errors[files[i]] = cfcatch>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
</cfloop>
|
||||
|
||||
<cfset testResult = suite.run()>
|
||||
|
||||
<cfset testResult.mergeErrorsIntoTestResult(this.s_results.Errors)>
|
||||
|
||||
<cfreturn testResult>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getTests" access="private" output="false">
|
||||
<cfargument name="directory" required="true" hint="directory of tests to run">
|
||||
<cfargument name="componentPath" required="true" hint="the component path to put in front of all tests found (i.e. 'com.blah')">
|
||||
<cfargument name="recurse" required="false" default="true" type="boolean" hint="whether to recurse down the directory tree">
|
||||
<cfargument name="excludes" required="false" default="" hint="ant-style syntax for excluding single files or directories">
|
||||
<cfset var q_tests = getDirectoryQuery(directory,recurse)>
|
||||
<cfset var a_tests = ArrayNew(1)>
|
||||
<cfset var testPath = "">
|
||||
|
||||
<cfloop query="q_tests">
|
||||
<cfset testPath = formatTestPath(arguments.directory, q_tests.directory & sep & q_tests.Name,arguments.componentPath)>
|
||||
|
||||
<cfif accept(testPath,arguments.excludes)>
|
||||
<cftry>
|
||||
<!---
|
||||
Compile first. This ensures that any new tests NOT found in the server cache get added.
|
||||
This may actually be quicker than searching the array and then conditionally compiling.
|
||||
Appears not to have much overhead, but, we should monitor anyway for large test runs.
|
||||
--->
|
||||
<cfset createObject("component","#testPath#") />
|
||||
<cfcatch type="any"><!--- no worries ---></cfcatch>
|
||||
</cftry>
|
||||
<cfset ArrayAppend(a_tests,testPath)>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<cfreturn a_tests>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="formatTestPath" access="private" output="false">
|
||||
<cfargument name="directory" required="true" hint="directory of tests to run">
|
||||
<cfargument name="fullFilePath">
|
||||
<cfargument name="componentPath" required="true" hint="the component path to put in front of all tests found (i.e. 'com.blah')">
|
||||
|
||||
<cfset var formatted = replaceNoCase(fullFilePath,directory,"")>
|
||||
<cfset formatted = reverse( replace(reverse(formatted),"cfc.","") )>
|
||||
<cfset formatted = componentPath & "." & formatted>
|
||||
<cfset formatted = reReplace(formatted, "(\\|/|\.){1,}" ,".","all")>
|
||||
<cfreturn formatted>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getDirectoryQuery" access="private" returntype="query" output="false">
|
||||
<cfargument name="directory" required="true" hint="directory of tests to run">
|
||||
<cfargument name="recurse" type="boolean" required="false" default="true" hint="whether to recurse down the directory tree">
|
||||
<cfset var files = "">
|
||||
<cfset var runnerUtils = "">
|
||||
|
||||
<cfif not DirectoryExists(arguments.directory)>
|
||||
<cfthrow message="Directory #directory# does not exist">
|
||||
</cfif>
|
||||
|
||||
<cfdirectory action="list" directory="#arguments.directory#" name="files" recurse="#arguments.recurse#" filter="*.cfc">
|
||||
|
||||
<cfreturn files>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getCatastrophicErrors" output="false">
|
||||
<cfreturn this.s_results.Errors>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="accept" hint="rudimentary initial implementation." access="private" output="false">
|
||||
<cfargument name="test" required="true">
|
||||
<cfargument name="excludes" required="true">
|
||||
|
||||
<cfset var testName = ListLast(arguments.test,".")>
|
||||
<cfset var thisExclude = "">
|
||||
|
||||
<cfif NOT reFindNoCase("^test",testName) AND NOT reFindNoCase("test$",testName)>
|
||||
<cfreturn false>
|
||||
</cfif>
|
||||
<cfif not len(excludes)>
|
||||
<cfreturn true>
|
||||
</cfif>
|
||||
|
||||
<cfset arguments.test = ListChangeDelims(arguments.test,"/",".")>
|
||||
<cfloop list="#excludes#" index="thisExclude" delimiters=",">
|
||||
<cfset thisExclude = replace(thisExclude,"\","/")>
|
||||
<cfif reFindNoCase("\b#thisExclude#\b",arguments.test)>
|
||||
<cfreturn false>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<cfreturn true>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="normalizeDirectory" hint="makes sure every directory has ending slashes" access="private" output="false">
|
||||
<cfargument name="Directory" type="string" required="true"/>
|
||||
<cfset var dir = createObject("java","java.io.File")>
|
||||
|
||||
<cfif not DirectoryExists(arguments.directory)>
|
||||
<cfset arguments.directory = expandPath(arguments.directory) />
|
||||
<cfif not DirectoryExists(arguments.directory)>
|
||||
<cfthrow message="Directory #directory# does not exist">
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfset dir.init(arguments.directory)>
|
||||
<cfset arguments.directory = dir.getCanonicalPath()>
|
||||
<cfset arguments.directory = replaceList(arguments.directory,"/,\","#sep#,#sep#")>
|
||||
<cfset arguments.directory = arguments.directory & sep>
|
||||
<cfreturn arguments.directory>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getComponentPath" access="remote" returntype="string" hint="Given a directory path, returns the corresponding CFC package according to CFMX" output="false">
|
||||
<cfargument name="path" type="string" required="true" />
|
||||
<cfargument name="refreshCache" type="boolean" required="false" default="false" />
|
||||
<cfscript>
|
||||
var explorer = createObject("component", "CFIDE.componentutils.cfcexplorer");
|
||||
var target = explorer.normalizePath(arguments.path);
|
||||
var cfcs = explorer.getcfcs(refreshCache); //true == refresh cache
|
||||
var cfc = "";
|
||||
var i = 1 ;
|
||||
var package = "";
|
||||
var cfcpath = "";
|
||||
|
||||
for(i = 1; i lte arraylen(cfcs); i = i+1){
|
||||
cfc = cfcs[i];
|
||||
//Assumes that CF always stores path info with fwd slash
|
||||
//and strip last element to get path.
|
||||
cfcpath = ListDeleteAt(cfc.path, listlen(cfc.path,"/"), "/");
|
||||
|
||||
//Array of structs. Doesn't seem possible to do binary search
|
||||
if(cfcpath eq target){
|
||||
|
||||
package = cfc.package;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return package;
|
||||
</cfscript>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction name="getComponentPathx" access="remote" returntype="string" hint="Given a directory path, returns the corresponding CFC package" output="false">
|
||||
<cfargument name="path" type="string" required="true" />
|
||||
<cfargument name="refreshCache" type="boolean" required="false" default="false" />
|
||||
|
||||
<cfset var files = '' />
|
||||
<cfset var package = '' />
|
||||
|
||||
<cfdirectory action="list" type="file" name="files" directory="#arguments.path#" filter="*.cfc" />
|
||||
|
||||
<cfif files.recordCount>
|
||||
<cfset package = replace(normalizePath(files.directory), '/', '.', 'all') />
|
||||
</cfif>
|
||||
|
||||
<cfreturn package />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="normalizePath" access="public" returntype="string" output="false">
|
||||
<cfargument name="path" type="string" required="true" />
|
||||
|
||||
<cfif not len(arguments.path)>
|
||||
<cfreturn arguments.path />
|
||||
</cfif>
|
||||
|
||||
<cfset arguments.path = replace(arguments.path, '\', '/', 'all') />
|
||||
|
||||
<cfif right(arguments.path, 1) eq '/'>
|
||||
<cfset arguments.path = left(arguments.path, len(arguments.path) - 1) />
|
||||
</cfif>
|
||||
|
||||
<cfreturn arguments.path />
|
||||
</cffunction>
|
||||
</cfcomponent>
|
||||
@@ -1,92 +0,0 @@
|
||||
<cfcomponent output="true">
|
||||
<cfsetting enablecfoutputonly="true" showdebugoutput="false">
|
||||
|
||||
<cffunction name="run" access="remote" output="true" returntype="void" hint="Generates and prints HTML, JUnit style XML, or XML data based on a directory of tests.">
|
||||
<cfargument name="type" type="string" hint="Specifies the type to run: TestCase(testcase) or Directory Path (dir)" required="true" />
|
||||
<cfargument name="value" type="string" hint="The value for the type: com.foo.MyTestCase or C:/my/tests/" required="true" />
|
||||
<cfargument name="packagename" type="string" hint="The package name for JUnitReportTask" required="false" default="mxunit.testresults" />
|
||||
<cfargument name="outputformat" type="string" hint="Valid Values: HTML,XML, or JUNITXML" required="false" default="junitxml" />
|
||||
<cfargument name="recurse" required="false" default="true" hint="whether to recurse down the directory tree">
|
||||
<cfargument name="excludes" required="false" default="" hint="List of files to exclude if type is DIR">
|
||||
<cfargument name="componentPath" required="false" hint="performance improver: pass the cfc-notation. See DirectoryTestSuite for details." default="">
|
||||
|
||||
<cfset var suite = createObject("component","mxunit.framework.TestSuite").TestSuite()/>
|
||||
<cfset var results = createObject("component","mxunit.framework.TestResult").TestResult()/>
|
||||
<cfscript>
|
||||
if(arguments.type is "testcase"){
|
||||
suite = createObject("component","mxunit.framework.TestSuite").TestSuite();
|
||||
suite.addAll(arguments.value);
|
||||
results = suite.run();
|
||||
}
|
||||
if(arguments.type is "dir"){
|
||||
//To Do: add args for recursion, includes, and excludes
|
||||
if(not isBoolean(arguments.recurse)){
|
||||
arguments.recurse = false;
|
||||
}
|
||||
|
||||
results = createObject("component","DirectoryTestSuite").run(directory=arguments.value, componentPath=arguments.componentPath, recurse=arguments.recurse, excludes=arguments.excludes);
|
||||
}
|
||||
//package name for JUnit reports
|
||||
results.setPackage(arguments.packagename);
|
||||
</cfscript>
|
||||
<!--- Read by Ant client and used to print summary to stdout --->
|
||||
<cfcookie name="mxunit_summary" value="#results.testRuns#,#results.testErrors#,#results.testFailures#,#results.totalExecutionTime#" />
|
||||
<!--- write the cookie first --->
|
||||
<cfset printResults(arguments.outputformat,results) />
|
||||
<cfreturn />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="printResults" access="private">
|
||||
<cfargument name="type">
|
||||
<cfargument name="results">
|
||||
<cfoutput>
|
||||
<cfswitch expression="#type#">
|
||||
<cfcase value="html">
|
||||
#trim(arguments.results.getHTMLResults())#
|
||||
</cfcase>
|
||||
<cfcase value="xml">
|
||||
#trim(arguments.results.getXMLResults())#
|
||||
</cfcase>
|
||||
<cfcase value="junitxml">
|
||||
#trim(arguments.results.getJUnitXMLResults())#
|
||||
</cfcase>
|
||||
<cfdefaultcase>#trim(arguments.results.getJUnitXMLResults())#</cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cfoutput>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="runFast" access="remote" output="true" returntype="void" hint="Generates and prints HTML, JUnit style XML, or XML data based on a directory of tests.">
|
||||
<cfargument name="type" type="string" hint="Specifies the type to run: TestCase(testcase) or Directory Path (dir)" required="true" />
|
||||
<cfargument name="value" type="string" hint="The value for the type: com.foo.MyTestCase or C:/my/tests/" required="true" />
|
||||
<cfargument name="packagename" type="string" hint="The package name for JUnitReportTask" required="false" default="mxunit.testresults" />
|
||||
<cfargument name="outputformat" type="string" hint="Valid Values: HTML,XML, or JUNITXML" required="false" default="junitxml" />
|
||||
<cfargument name="recurse" required="false" default="true" hint="whether to recurse down the directory tree">
|
||||
<cfargument name="excludes" required="false" default="" hint="List of files to exclude if type is DIR">
|
||||
<cfargument name="componentPath" required="false" hint="performance improver: pass the cfc-notation. See DirectoryTestSuite for details." default="">
|
||||
|
||||
<cfset var suite = createObject("component","mxunit.framework.TestSuite").TestSuite()/>
|
||||
<cfset var results = createObject("component","mxunit.framework.TestResult").TestResult()/>
|
||||
<cfscript>
|
||||
if(arguments.type is "testcase"){
|
||||
suite = createObject("component","mxunit.framework.TestSuite").TestSuite();
|
||||
suite.addAll(arguments.value);
|
||||
results = suite.run();
|
||||
}
|
||||
if(arguments.type is "dir"){
|
||||
//To Do: add args for recursion, includes, and excludes
|
||||
if(not isBoolean(arguments.recurse)){
|
||||
arguments.recurse = false;
|
||||
}
|
||||
|
||||
results = createObject("component","DirectoryTestSuite").run(directory=arguments.value, componentPath=arguments.componentPath, recurse=arguments.recurse, excludes=arguments.excludes);
|
||||
}
|
||||
//package name for JUnit reports
|
||||
results.setPackage(arguments.packagename);
|
||||
</cfscript>
|
||||
<!--- Read by Ant client and used to print summary to stdout --->
|
||||
<cfcookie name="mxunit_summary" value="#results.testRuns#,#results.testErrors#,#results.testFailures#,#results.totalExecutionTime#" />
|
||||
<!--- write the cookie first
|
||||
<cfset printResults(arguments.outputformat,results) /> --->
|
||||
<cfreturn />
|
||||
</cffunction>
|
||||
</cfcomponent>
|
||||
@@ -1 +0,0 @@
|
||||
<cfcomponent extends="mxunit.framework.RemoteFacade"></cfcomponent>
|
||||
@@ -1,22 +0,0 @@
|
||||
<cfcomponent extends="base">
|
||||
|
||||
<cffunction name="setup">
|
||||
<cfset reloadFramework()>
|
||||
</cffunction>
|
||||
|
||||
<cfscript>
|
||||
function beforeTests(){
|
||||
variables.taffy = createObject("component","taffy.tests.Application");
|
||||
makePublic(variables.taffy, "getBeanFactory");
|
||||
variables.factory = variables.taffy.getBeanFactory();
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resourcesConflict'), 'taffy.tests.resourcesConflict', expandPath('/taffy/tests/resourcesConflict'), true, variables.taffy);
|
||||
|
||||
}
|
||||
|
||||
function test_conflicting_URIs_get_skipped() {
|
||||
assertTrue(checkIfOneSkippedRessourceContainsExpectedException("errorCode", "taffy.resources.DuplicateUriPattern"), "Conflicting URIs not showing in errors");
|
||||
}
|
||||
</cfscript>
|
||||
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,645 +0,0 @@
|
||||
<cfcomponent extends="base">
|
||||
|
||||
<cffunction name="setup">
|
||||
<cfset reloadFramework()>
|
||||
</cffunction>
|
||||
|
||||
<cfscript>
|
||||
function beforeTests(){
|
||||
variables.taffy = createObject("component","taffy.tests.Application");
|
||||
makePublic(variables.taffy, "getBeanFactory");
|
||||
variables.factory = variables.taffy.getBeanFactory();
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resources'), 'taffy.tests.resources', expandPath('/taffy/tests/resources'), true );
|
||||
}
|
||||
|
||||
function test_properly_notifies_unimplemented_mimes(){
|
||||
makePublic(variables.taffy, "mimeSupported");
|
||||
// debug(variables.taffy);
|
||||
// debug(application);
|
||||
assertFalse(taffy.mimeSupported("DoesNotExist"), "When given a mime type that should not exist, Taffy reported that it did.");
|
||||
}
|
||||
|
||||
function test_properly_notifies_implemented_mimes(){
|
||||
makePublic(variables.taffy, "mimeSupported");
|
||||
makePublic(variables.taffy, "inspectMimeTypes");
|
||||
variables.taffy.inspectMimeTypes('taffy.core.nativeJsonSerializer', variables.taffy.getBeanFactory());
|
||||
assertTrue(taffy.mimeSupported("json"));
|
||||
assertTrue(taffy.mimeSupported("text/json"));
|
||||
assertTrue(taffy.mimeSupported("application/json"));
|
||||
}
|
||||
|
||||
function test_returns_access_control_expose_headers_header(){
|
||||
local.result = apiCall("get", "/echo/foo.json", "");
|
||||
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "Access-Control-Expose-Headers"));
|
||||
debug(local.result.responseHeader["Access-Control-Expose-Headers"]);
|
||||
assertTrue(findNoCase("Etag", local.result.responseHeader["Access-Control-Expose-Headers"]));
|
||||
}
|
||||
|
||||
function test_returns_etag_header(){
|
||||
//both requests should yield the same etag header
|
||||
local.result = apiCall("get", "/echo/foo.json", "");
|
||||
local.result2 = apiCall("get", "/echo/foo.json", "");
|
||||
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "Etag"));
|
||||
assertTrue(structKeyExists(local.result2.responseHeader, "Etag"));
|
||||
assertEquals(local.result2.responseHeader.etag, local.result.responseHeader.etag);
|
||||
}
|
||||
|
||||
function test_returns_304_when_not_modified(){
|
||||
local.result = apiCall("get", "/echo/foo.json", "");
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "Etag"));
|
||||
|
||||
local.h = {};
|
||||
local.h['if-none-match'] = local.result.responseHeader.etag;
|
||||
local.result = apiCall("get", "/echo/foo.json", "", local.h);
|
||||
debug(local.result);
|
||||
assertEquals(304, val(local.result.responseHeader.status_code));
|
||||
}
|
||||
|
||||
function test_json_result_is_json(){
|
||||
local.result = apiCall ("get","/echo/2.json","bar=foo");
|
||||
// debug(local.result);
|
||||
assertTrue(isJson(local.result.fileContent), "Expected JSON content back but was not able to identify it as such.");
|
||||
}
|
||||
|
||||
function test_custom_status_is_returned(){
|
||||
local.result = apiCall("get", "/echo/1.json?foo=bar", "");
|
||||
// debug(local.result);
|
||||
// debug(application);
|
||||
assertEquals(999, local.result.responseHeader.status_code, "Expected status code 999 but got something else.");
|
||||
}
|
||||
|
||||
function test_custom_headers_work(){
|
||||
local.result = apiCall("get", "/echo/-1.json", "");
|
||||
// debug(local.result);
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "x-dude"), "Expected response header `x-dude` but it was not included.");
|
||||
}
|
||||
|
||||
function test_global_headers_work(){
|
||||
local.result = apiCall("get", "/echo/1.json", "");
|
||||
// debug(local.result);
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "x-foo-globalheader"), "Expected response header `x-foo-globalheader` but it was not included.");
|
||||
}
|
||||
|
||||
function test_deserializer_inspection_finds_all_content_types(){
|
||||
makePublic(variables.taffy, "getSupportedContentTypes");
|
||||
local.result = variables.taffy.getSupportedContentTypes("taffy.core.baseDeserializer");
|
||||
// debug(local.result);
|
||||
assertTrue(structKeyExists(local.result, "application/x-www-form-urlencoded"));
|
||||
local.result = variables.taffy.getSupportedContentTypes("taffy.core.nativeJsonDeserializer");
|
||||
// debug(local.result);
|
||||
assertTrue(structKeyExists(local.result, "application/json"));
|
||||
assertTrue(structKeyExists(local.result, "text/json"));
|
||||
assertTrue(structKeyExists(local.result, "application/x-www-form-urlencoded"));
|
||||
}
|
||||
|
||||
function test_deserializer_support_detection_works(){
|
||||
makePublic(variables.taffy, "contentTypeIsSupported");
|
||||
// debug(application._taffy.contentTypes);
|
||||
assertTrue(variables.taffy.contentTypeIsSupported("application/json"));
|
||||
assertTrue(variables.taffy.contentTypeIsSupported("application/json;v=1"));
|
||||
assertFalse(variables.taffy.contentTypeIsSupported("application/does-not-exist"));
|
||||
}
|
||||
|
||||
function test_uri_regexes_are_correct(){
|
||||
makePublic(variables.taffy, "convertURItoRegex");
|
||||
|
||||
local.result = taffy.convertURItoRegex("/a/{abc}/b");
|
||||
// debug(local.result);
|
||||
assertEquals( "^/a/([^\/]+)/b((?:\.)[^\.\?\/]+)?\/?$", local.result["uriregex"], "Resulted regex did not match expected. (assert 1)");
|
||||
assertEquals( 1, arrayLen(local.result["tokens"]), "assert 2" );
|
||||
assertEquals( "abc", local.result["tokens"][1], "assert 3" );
|
||||
|
||||
local.result2 = taffy.convertURItoRegex("/a/{abc}");
|
||||
// debug(local.result2);
|
||||
assertEquals( "^/a/(?:(?:([^\/\.]+)(?:\.)([a-za-z0-9]+))\/?|([^\/\.]+))((?:\.)[^\.\?\/]+)?\/?$", local.result2["uriregex"], "Resulted regex did not match expected.");
|
||||
assertEquals( 1, arrayLen(local.result2["tokens"]) );
|
||||
assertEquals( "abc", local.result2["tokens"][1] );
|
||||
|
||||
//custom regexes for tokens
|
||||
local.result3 = taffy.convertURItoRegex("/a/{b:[a-z]+(?:42){1}}");
|
||||
// debug(local.result3);
|
||||
assertEquals( "^/a/([a-z]+(?:42){1})((?:\.)[^\.\?\/]+)?\/?$", local.result3["uriregex"], "Resulted regex did not match expected. (assert 7)");
|
||||
assertEquals( 1, arrayLen(local.result3["tokens"]), "assert 8" );
|
||||
assertEquals( "b", local.result3["tokens"][1], "assert 9" );
|
||||
|
||||
local.result4 = taffy.convertURItoRegex("/a/{b:[0-4]{1,7}(?:aaa){1}}/c/{d:\d+}");
|
||||
// debug(local.result4);
|
||||
assertEquals( "^/a/([0-4]{1,7}(?:aaa){1})/c/(\d+)((?:\.)[^\.\?\/]+)?\/?$", local.result4["uriregex"], "Resulted regex did not match expected. (assert 10)");
|
||||
assertEquals( 2, arrayLen(local.result4["tokens"]), "assert 11" );
|
||||
assertEquals( "b", local.result4["tokens"][1], "assert 12" );
|
||||
assertEquals( "d", local.result4["tokens"][2], "assert 13" );
|
||||
}
|
||||
|
||||
function test_uri_matching_works_with_extension(){
|
||||
makePublic(variables.taffy, "matchURI");
|
||||
local.result = variables.taffy.matchURI("/echo/3.json");
|
||||
// debug(local.result);
|
||||
assertEquals('^/echo/(?:(?:([^\/\.]+)(?:\.)([a-za-z0-9]+))\/?|([^\/\.]+))((?:\.)[^\.\?\/]+)?\/?$', local.result);
|
||||
}
|
||||
|
||||
function test_uri_matching_works_without_extension(){
|
||||
makePublic(variables.taffy, "matchURI");
|
||||
local.result = variables.taffy.matchURI("/echo/3");
|
||||
// debug(local.result);
|
||||
assertEquals('^/echo/(?:(?:([^\/\.]+)(?:\.)([a-za-z0-9]+))\/?|([^\/\.]+))((?:\.)[^\.\?\/]+)?\/?$', local.result);
|
||||
}
|
||||
|
||||
function test_uri_matching_works_with_trailing_slash_with_extension(){
|
||||
makePublic(variables.taffy, "matchURI");
|
||||
local.result = variables.taffy.matchURI("/echo/3.json/");
|
||||
// debug(local.result);
|
||||
assertEquals('^/echo/(?:(?:([^\/\.]+)(?:\.)([a-za-z0-9]+))\/?|([^\/\.]+))((?:\.)[^\.\?\/]+)?\/?$', local.result);
|
||||
}
|
||||
|
||||
function test_uri_matching_works_with_trailing_slash_without_extension(){
|
||||
makePublic(variables.taffy, "matchURI");
|
||||
local.result = variables.taffy.matchURI("/echo/3/");
|
||||
// debug(local.result);
|
||||
assertEquals('^/echo/(?:(?:([^\/\.]+)(?:\.)([a-za-z0-9]+))\/?|([^\/\.]+))((?:\.)[^\.\?\/]+)?\/?$', local.result);
|
||||
}
|
||||
|
||||
function test_uri_matching_is_sorted_so_static_URIs_take_priority_over_tokens(){
|
||||
makePublic(variables.taffy, "matchURI");
|
||||
local.result = variables.taffy.matchURI("/echo/3");
|
||||
// debug(local.result);
|
||||
assertEquals('^/echo/(?:(?:([^\/\.]+)(?:\.)([a-za-z0-9]+))\/?|([^\/\.]+))((?:\.)[^\.\?\/]+)?\/?$', local.result);
|
||||
local.result = variables.taffy.matchURI("/echo/towel");
|
||||
// debug(local.result);
|
||||
assertEquals('^/echo/towel((?:\.)[^\.\?\/]+)?\/?$', local.result);
|
||||
}
|
||||
|
||||
function test_request_parsing_works(){
|
||||
makePublic(variables.taffy,"buildRequestArguments");
|
||||
local.result = variables.taffy.buildRequestArguments(
|
||||
regex = '/echo/([^\/\.]+)$',
|
||||
tokenNamesArray = listToArray("id"),
|
||||
uri = '/echo/16',
|
||||
queryString = 'foo=bar&bar=foo',
|
||||
headers = structNew()
|
||||
);
|
||||
// debug(local.result);
|
||||
assertTrue(structKeyExists(local.result, "foo") && local.result.foo == "bar", "Missing or incorrect value for key `foo`.");
|
||||
assertTrue(structKeyExists(local.result, "bar") && local.result.bar == "foo", "Missing or incorrect value for key `bar`.");
|
||||
assertTrue(structKeyExists(local.result, "id") && local.result.id == 16, "Missing or incorrect value for key `id`.");
|
||||
}
|
||||
|
||||
function test_properly_decodes_urlEncoded_put_request_body(){
|
||||
local.result = apiCall("put", "/echo/99.json", "foo=bar&check=mate");
|
||||
// debug(local.result);
|
||||
if (!isJson(local.result.fileContent)){
|
||||
// debug(local.result.fileContent);
|
||||
fail("Result was not JSON");
|
||||
return local.result.fileContent;
|
||||
}
|
||||
local.result = deserializeJSON(local.result.fileContent);
|
||||
assertTrue(structKeyExists(local.result, "foo") && local.result.foo == "bar", "Missing or incorrect value for key `foo`.");
|
||||
assertTrue(structKeyExists(local.result, "check") && local.result.check == "mate", "Missing or incorrect value for key `check`.");
|
||||
}
|
||||
|
||||
function test_properly_decodes_json_put_request_body(){
|
||||
local.result = apiCall("put", "/echo/99.json", '{"foo":"bar"}');
|
||||
// debug(local.result);
|
||||
if (!isJson(local.result.fileContent)){
|
||||
fail("Result was not JSON");
|
||||
return;
|
||||
}
|
||||
local.result = deserializeJSON(local.result.fileContent);
|
||||
// debug(local.result);
|
||||
assertTrue(structKeyExists(local.result, "foo") && local.result.foo == "bar", "Missing or incorrect value for key `foo`.");
|
||||
}
|
||||
|
||||
function test_properly_decodes_json_post_request_body(){
|
||||
local.result = apiCall("post", "/echo/99.json", '{"foo":"bar"}');
|
||||
// debug(local.result);
|
||||
if (!isJson(local.result.fileContent)){
|
||||
fail("Result was not JSON");
|
||||
return;
|
||||
}
|
||||
local.result = deserializeJSON(local.result.fileContent);
|
||||
// debug(local.result);
|
||||
assertTrue(structKeyExists(local.result, "foo") && local.result.foo == "bar", "Missing or incorrect value for key `foo`.");
|
||||
}
|
||||
|
||||
function test_returns_error_when_requested_mime_not_supported(){
|
||||
local.h = structNew();
|
||||
local.h['Accept'] = "application/NOPE";
|
||||
local.result = apiCall ("get","/echo/2","foo=bar", local.h);
|
||||
// debug(local.result);
|
||||
assertEquals(400, local.result.responseHeader.status_code);
|
||||
assertEquals("Requested mime type is not supported (application/NOPE)", local.result.responseHeader.explanation);
|
||||
}
|
||||
|
||||
function test_extension_takes_precedence_over_accept_header(){
|
||||
local.headers = structNew();
|
||||
local.headers["Accept"] = "text/xml";
|
||||
local.result = apiCall("get","/echo/2.json","foo=bar",local.headers);
|
||||
// debug(local.result);
|
||||
assertEquals(999, local.result.responseHeader.status_code);
|
||||
assertTrue(isJson(local.result.fileContent));
|
||||
}
|
||||
|
||||
function test_allows_regex_as_final_url_value(){
|
||||
makePublic(variables.taffy, "buildRequestArguments");
|
||||
local.headers = structNew();
|
||||
local.headers.Accept = "application/json";
|
||||
local.tokenArray = arrayNew(1);
|
||||
arrayAppend(local.tokenArray, "id");
|
||||
local.result = variables.taffy.buildRequestArguments(
|
||||
"^/echo/([a-zA-Z0-9_\-\.\+]+@[a-zA-Z0-9_\-\.]+\.?[a-zA-Z]+)((?:\.)[^\.\?]+)?$",
|
||||
local.tokenArray,
|
||||
"/echo/foo@bar.com",
|
||||
"",
|
||||
local.headers
|
||||
);
|
||||
// debug(local.result);
|
||||
assertTrue(local.result._taffy_mime eq "json", "Did not detect desired return format correctly.");
|
||||
|
||||
//full integration test for a@b.c.json
|
||||
local.result = apiCall("get", "/echo_regex/12345.json", "", {});
|
||||
// debug(local.result);
|
||||
assert(isJson(local.result.fileContent), "response was not json");
|
||||
local.response = deserializeJSON(local.result.fileContent);
|
||||
assertEquals("12345", local.response.id);
|
||||
|
||||
//full integration test for a@b.c (no .json, but with headers)
|
||||
local.result = apiCall("get", "/echo_regex/12345", "", local.headers);
|
||||
// debug(local.result);
|
||||
assert(isJson(local.result.fileContent), "response was not json");
|
||||
local.response = deserializeJSON(local.result.fileContent);
|
||||
assertEquals("12345", local.response.id);
|
||||
}
|
||||
|
||||
function test_returns_405_for_unimplemented_verbs(){
|
||||
local.result = apiCall("delete", "/echo/2.json", "foo=bar");
|
||||
// debug(local.result);
|
||||
assertEquals(405, local.result.responseHeader.status_code);
|
||||
}
|
||||
|
||||
function test_test_onTaffyRequest_allow(){
|
||||
local.result = apiCall("get","/echo/12.json","refuse=false");
|
||||
// debug(local.result);
|
||||
assertEquals(999,local.result.responseHeader.status_code);
|
||||
}
|
||||
|
||||
function test_onTaffyRequest_deny(){
|
||||
local.result = apiCall("get","/echo/12.json","refuse=true");
|
||||
// debug(local.result);
|
||||
assertEquals(405,local.result.responseHeader.status_code);
|
||||
}
|
||||
|
||||
function test_getCacheKey_customBehavior() {
|
||||
local.result = variables.taffy.getCacheKey(
|
||||
"EchoMember",
|
||||
{ "foo": "bar" },
|
||||
"/echo/12.json"
|
||||
);
|
||||
|
||||
assertEquals("echomember_foo", local.result);
|
||||
}
|
||||
|
||||
function test_getCacheKey_defaultBehavior() {
|
||||
local.args = {
|
||||
cfc: "EchoMember",
|
||||
requestArguments: { "default": true },
|
||||
matchedURI: "/echo/12.json"
|
||||
};
|
||||
|
||||
local.result = variables.taffy.getCacheKey(argumentCollection = local.args);
|
||||
|
||||
// ACF and Lucee generate hash code differently
|
||||
assertEquals("/echo/12.json_#local.args.requestArguments.hashCode()#", local.result);
|
||||
}
|
||||
|
||||
function test_external_file_request_passes_through(){
|
||||
local.result = getUrl('http://#CGI.SERVER_NAME#:#CGI.SERVER_PORT##replace(cgi.script_name, "/tests/tests/run.cfm", "/tests/someFolder/someOtherFile.cfm")#');
|
||||
debug(local.result);
|
||||
assertTrue(findNoCase('woot', local.result.fileContent), "Was not able to get the DMZ file.");
|
||||
}
|
||||
|
||||
function test_tunnel_PUT_through_POST(){
|
||||
var local = {};
|
||||
|
||||
local.headers["X-HTTP-Method-Override"] = "PUT";
|
||||
local.result = apiCall("post","/echo/tunnel/12.json","", local.headers);
|
||||
// debug(local.result);
|
||||
assertEquals(200,local.result.responseHeader.status_code);
|
||||
|
||||
local.deserializedContent = deserializeJSON( local.result.fileContent );
|
||||
// debug( local.deserializedContent );
|
||||
assertEquals("put", local.deserializedContent.actualMethod);
|
||||
}
|
||||
|
||||
function test_tunnel_DELETE_through_POST(){
|
||||
var local = {};
|
||||
|
||||
local.headers["X-HTTP-Method-Override"] = "DELETE";
|
||||
local.result = apiCall("post","/echo/tunnel/12.json","", local.headers);
|
||||
// debug(local.result);
|
||||
assertEquals(200,local.result.responseHeader.status_code);
|
||||
|
||||
local.deserializedContent = deserializeJSON( local.result.fileContent );
|
||||
// debug( local.deserializedContent );
|
||||
assertEquals("delete", local.deserializedContent.actualMethod);
|
||||
}
|
||||
|
||||
function test_put_body_is_mime_content(){
|
||||
var local = {};
|
||||
|
||||
local.result = apiCall(
|
||||
"put",
|
||||
"/echo/12.json",
|
||||
'{"foo":"The quick brown fox jumped over the lazy dog."}'
|
||||
);
|
||||
// debug(local.result);
|
||||
assertEquals(200,local.result.responseHeader.status_code);
|
||||
|
||||
local.deserializedContent = deserializeJSON( local.result.fileContent );
|
||||
// debug( local.deserializedContent );
|
||||
|
||||
// The service response should contain only the ID parameter, and not anything parsed from the body
|
||||
assertEquals("foo,id,password,username", listSort(structKeylist(local.deserializedContent), "textnocase"));
|
||||
assertEquals(12, local.deserializedContent["id"]);
|
||||
assertEquals("The quick brown fox jumped over the lazy dog.", local.deserializedContent["foo"]);
|
||||
}
|
||||
|
||||
function test_put_body_is_url_encoded_params(){
|
||||
var local = {};
|
||||
local.result = apiCall(
|
||||
"put",
|
||||
"/echo/12.json",
|
||||
"foo=yankee&bar=hotel&baz=foxtrot"
|
||||
);
|
||||
// debug(local.result);
|
||||
assertEquals(200,local.result.responseHeader.status_code);
|
||||
|
||||
local.deserializedContent = deserializeJSON( local.result.fileContent );
|
||||
// debug( local.deserializedContent );
|
||||
|
||||
// The service response should contain the ID parameter and all parsed form fields from the body
|
||||
local.sortedKeys = listSort(structKeylist(local.deserializedContent), "textnocase");
|
||||
//because apparently railo includes fieldnames when ACF doesn't...
|
||||
assertTrue("bar,baz,foo,id,password,username" eq local.sortedKeys or "bar,baz,fieldnames,foo,id,password,username" eq local.sortedKeys);
|
||||
assertEquals(12, local.deserializedContent["id"]);
|
||||
assertEquals("yankee", local.deserializedContent["foo"]);
|
||||
assertEquals("hotel", local.deserializedContent["bar"]);
|
||||
assertEquals("foxtrot", local.deserializedContent["baz"]);
|
||||
}
|
||||
|
||||
function test_get_queryString_keys_without_values_returns_empty_string() {
|
||||
makePublic(variables.taffy, "buildRequestArguments");
|
||||
|
||||
var returnedArguments = variables.taffy.buildRequestArguments(
|
||||
regex = "^/testResource/$",
|
||||
tokenNamesArray = [],
|
||||
uri = "/testResource/",
|
||||
queryString = "keyOne=valueOne&keyTwo=&keyThree=valueThree",
|
||||
headers = {}
|
||||
);
|
||||
|
||||
assertEquals("", returnedArguments["keyTwo"]);
|
||||
}
|
||||
|
||||
function test_returns_allow_header_for_405(){
|
||||
local.result = apiCall("delete","/echo/12.json","");
|
||||
// debug(local.result);
|
||||
assertEquals(405,local.result.responseHeader.status_code);
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "allow"),"Expected ALLOW header, but couldn't find it");
|
||||
}
|
||||
|
||||
function test_returns_allow_header_for_get_200(){
|
||||
local.result = apiCall("get","/echo/tunnel/12.json","");
|
||||
// debug(local.result);
|
||||
assertEquals(200,local.result.responseHeader.status_code);
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "allow"),"Expected ALLOW header, but couldn't find it");
|
||||
}
|
||||
|
||||
function test_returns_allow_header_for_post_201(){
|
||||
local.result = apiCall("post","/echo/tunnel/12.json","");
|
||||
// debug(local.result);
|
||||
assertEquals(201,local.result.responseHeader.status_code);
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "allow"),"Expected ALLOW header, but couldn't find it");
|
||||
}
|
||||
|
||||
function test_returns_allow_header_for_put_200(){
|
||||
local.result = apiCall("put","/echo/tunnel/12.json","");
|
||||
// debug(local.result);
|
||||
assertEquals(200,local.result.responseHeader.status_code);
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "allow"),"Expected ALLOW header, but couldn't find it");
|
||||
}
|
||||
|
||||
function test_returns_allow_header_for_delete_200(){
|
||||
local.result = apiCall("delete","/echo/tunnel/12.json","");
|
||||
// debug(local.result);
|
||||
assertEquals(200,local.result.responseHeader.status_code);
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "allow"),"Expected ALLOW header, but couldn't find it");
|
||||
}
|
||||
|
||||
function test_can_pass_data_from_onTaffyRequest_to_resource(){
|
||||
local.result = apiCall("get", "/echo/dude.json", "hulk=smash");
|
||||
// debug(local.result);
|
||||
local.body = deserializeJSON(local.result.fileContent);
|
||||
assertTrue(structKeyExists(local.body, "dataFromOTR"));
|
||||
assertTrue(local.body.dataFromOTR eq "who let the hulk out?!");
|
||||
}
|
||||
|
||||
function test_reload_on_every_request_setting_works(){
|
||||
application._taffy.settings.reloadOnEveryRequest = false;
|
||||
local.result = apiCall("get", "/echo/dude.json", "");
|
||||
// debug(local.result);
|
||||
assertFalse(structKeyExists(local.result.responseheader, "X-TAFFY-RELOADED"), "Expected reload header to be missing, but it was sent.");
|
||||
application._taffy.settings.reloadOnEveryRequest = true;
|
||||
local.result2 = apiCall("get", "/echo/dude.json", "");
|
||||
// debug(local.result2);
|
||||
assertTrue(structKeyExists(local.result2.responseheader, "X-TAFFY-RELOADED"), "Expected reload header to be sent, but it was missing.");
|
||||
}
|
||||
|
||||
function test_returns_error_when_resource_throws_exception(){
|
||||
local.result = apiCall("get", "/throwException.json", "");
|
||||
// debug(local.result);
|
||||
assertEquals(500, local.result.responseHeader.status_code);
|
||||
assertTrue( isJson( local.result.fileContent ), "Response body was not json" );
|
||||
}
|
||||
|
||||
function test_basic_auth_credentials_found(){
|
||||
local.result = apiCall("get", "/basicauth.json", "", {}, "Towel:42");
|
||||
// debug(local.result);
|
||||
assertTrue(isJson(local.result.fileContent));
|
||||
local.data = deserializeJSON(local.result.fileContent);
|
||||
assertTrue(structKeyExists(local.data, "username"));
|
||||
assertEquals("Towel", local.data.username);
|
||||
assertTrue(structKeyExists(local.data, "password"));
|
||||
assertEquals("42", local.data.password);
|
||||
}
|
||||
|
||||
function test_getHostname_returns_not_blank(){
|
||||
local.hostname = variables.taffy.getHostname();
|
||||
// debug(local.hostname);
|
||||
assertNotEquals( "", local.hostname );
|
||||
}
|
||||
|
||||
function test_envConfig_is_applied(){
|
||||
// debug( application._taffy.settings.reloadPassword );
|
||||
assertEquals( "dontpanic", application._taffy.settings.reloadPassword );
|
||||
}
|
||||
|
||||
function test_use_endpointURLParam_in_GET(){
|
||||
local.result = apiCall('get','?#application._taffy.settings.endpointURLParam#=/echo/2606.json','');
|
||||
|
||||
// debug(local.result);
|
||||
assertEquals(999,val(local.result.statusCode));
|
||||
}
|
||||
|
||||
function test_use_endpointURLParam_in_POST(){
|
||||
local.result = apiCall('post','?#application._taffy.settings.endpointURLParam#=/echo/2606.json','bar=foo');
|
||||
|
||||
// debug(local.result);
|
||||
assertEquals(200,val(local.result.statusCode));
|
||||
}
|
||||
|
||||
function test_use_endpointURLParam_in_PUT(){
|
||||
local.result = apiCall('put','?#application._taffy.settings.endpointURLParam#=/echo/2606.json','bar=foo');
|
||||
|
||||
// debug(local.result);
|
||||
assertEquals(200,val(local.result.statusCode));
|
||||
}
|
||||
|
||||
function test_use_endpointURLParam_in_DELETE(){
|
||||
local.result = apiCall('delete','?#application._taffy.settings.endpointURLParam#=/echo/tunnel/2606.json','');
|
||||
|
||||
// debug(local.result);
|
||||
assertEquals(200,val(local.result.statusCode));
|
||||
}
|
||||
|
||||
function test_allows_dashboard_when_enabled(){
|
||||
var restore = application._taffy.settings.disableDashboard;
|
||||
application._taffy.settings.disableDashboard = false;
|
||||
local.result = apiCall("get", "/", "");
|
||||
// debug(local.result);
|
||||
assertEquals(200, val(local.result.statusCode));
|
||||
|
||||
application._taffy.settings.disableDashboard = restore;
|
||||
}
|
||||
|
||||
function test_returns_403_at_root_when_dashboard_disabled_with_no_redirect(){
|
||||
var restore = application._taffy.settings.disableDashboard;
|
||||
application._taffy.settings.disableDashboard = true;
|
||||
local.result = apiCall("get", "/", "");
|
||||
// debug(local.result);
|
||||
assertEquals(403, val(local.result.statusCode));
|
||||
|
||||
application._taffy.settings.disableDashboard = restore;
|
||||
}
|
||||
|
||||
function test_returns_302_at_root_when_dashboard_disabled_with_redirect(){
|
||||
var restore1 = application._taffy.settings.disableDashboard;
|
||||
var restore2 = application._taffy.settings.disabledDashboardRedirect;
|
||||
application._taffy.settings.disableDashboard = true;
|
||||
application._taffy.settings.disabledDashboardRedirect = 'https://google.com';
|
||||
local.result = apiCall("get", "/", "");
|
||||
// debug(local.result);
|
||||
assertEquals(302, val(local.result.statusCode));
|
||||
assertTrue(structKeyExists(local.result.responseHEader, "location"));
|
||||
assertEquals(application._taffy.settings.disabledDashboardRedirect, local.result.responseHeader.location);
|
||||
|
||||
application._taffy.settings.disableDashboard = restore1;
|
||||
application._taffy.settings.disabledDashboardRedirect = restore2;
|
||||
}
|
||||
|
||||
function test_properly_returns_wrapped_jsonp(){
|
||||
application._taffy.settings.jsonp = "callback";
|
||||
local.result = apiCall("get", "/echo/dude.json?callback=zomg", '');
|
||||
// debug(local.result);
|
||||
assertEquals('zomg(', left(local.result.fileContent, 5), "Does not begin with call to jsonp callback");
|
||||
assertEquals(");", right(local.result.fileContent, 2), "Does not end with `);`");
|
||||
}
|
||||
|
||||
function test_properly_handles_arbitrary_cors_headers(){
|
||||
//see: https://github.com/atuttle/Taffy/issues/144
|
||||
application._taffy.settings.allowCrossDomain = true;
|
||||
local.h = { "Access-Control-Request-Headers" = "goat, pigeon, man-bear-pig", "Origin":"http://#cgi.server_name#/"};
|
||||
local.result = apiCall("get", "/echo/dude.json", "", local.h);
|
||||
//debug(local.result);
|
||||
assertTrue(local.result.responseHeader["Access-Control-Allow-Headers"] contains "goat");
|
||||
assertTrue(local.result.responseHeader["Access-Control-Allow-Headers"] contains "pigeon");
|
||||
assertTrue(local.result.responseHeader["Access-Control-Allow-Headers"] contains "man-bear-pig");
|
||||
}
|
||||
|
||||
function test_properly_handles_arbitrary_cors_headers_on_error(){
|
||||
//see: https://github.com/atuttle/Taffy/issues/159
|
||||
application._taffy.settings.allowCrossDomain = true;
|
||||
local.h = { "Access-Control-Request-Headers" = "goat, pigeon, man-bear-pig", "Origin":"http://#cgi.server_name#/"};
|
||||
local.result = apiCall("get", "/throwException.json", "", local.h);
|
||||
// debug(local.result);
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "Access-Control-Allow-Origin"));
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "Access-Control-Allow-Methods"));
|
||||
assertTrue(structKeyExists(local.result.responseHeader, "Access-Control-Allow-Headers"));
|
||||
assertTrue(local.result.responseHeader["Access-Control-Allow-Headers"] contains "goat");
|
||||
assertTrue(local.result.responseHeader["Access-Control-Allow-Headers"] contains "pigeon");
|
||||
assertTrue(local.result.responseHeader["Access-Control-Allow-Headers"] contains "man-bear-pig");
|
||||
}
|
||||
|
||||
function test_non_struct_json_body_sent_to_resource_as_underscore_body(){
|
||||
//see: https://github.com/atuttle/Taffy/issues/169
|
||||
local.result = apiCall("post", "/echo/5", "[1,2,3]");
|
||||
// debug(local.result);
|
||||
local.response = deserializeJSON(local.result.fileContent);
|
||||
assertTrue(structKeyExists(local.response, "_body"));
|
||||
assertTrue(isArray(local.response._body));
|
||||
assertTrue(arrayLen(local.response._body) == 3);
|
||||
}
|
||||
|
||||
function test_comma_delim_list_of_uris_for_alias(){
|
||||
|
||||
//works with /echo_alias/{ID}
|
||||
local.result = apiCall("get", "/echo_alias/4", "");
|
||||
//debug(local.result);
|
||||
|
||||
assertEquals(200, val(local.result.statusCode));
|
||||
assertEquals(serializeJSON({ID="4"}), local.result.fileContent);
|
||||
|
||||
//works with /echo_alias
|
||||
local.result = apiCall("get", "/echo_alias", "");
|
||||
// debug(local.result);
|
||||
assertEquals(200, val(local.result.statusCode));
|
||||
assertEquals(serializeJSON({ID="0"}), local.result.fileContent);
|
||||
|
||||
//works with /echo_alias/ (trailing slash)
|
||||
local.result = apiCall("get", "/echo_alias/", "");
|
||||
// debug(local.result);
|
||||
assertEquals(200, val(local.result.statusCode));
|
||||
assertEquals(serializeJSON({ID="0"}), local.result.fileContent);
|
||||
|
||||
//works with /echo_alias?ID=x
|
||||
local.result = apiCall("get", "/echo_alias", "ID=2");
|
||||
// debug(local.result);
|
||||
assertEquals(200, val(local.result.statusCode));
|
||||
assertEquals(serializeJSON({ID="2"}), local.result.fileContent);
|
||||
}
|
||||
</cfscript>
|
||||
|
||||
|
||||
<cffunction name="test_can_upload_a_file">
|
||||
<cfset var local = structNew() />
|
||||
<cfset local.apiRootURL = getDirectoryFromPath(cgi.script_name) />
|
||||
<cfset local.apiRootURL = listDeleteAt(local.apiRootURL,listLen(local.apiRootURL,'/'),'/') />
|
||||
<cfhttp
|
||||
url="http://#cgi.server_name#:#cgi.server_port##local.apiRootURL#/index.cfm/upload"
|
||||
method="post"
|
||||
result="local.uploadResult">
|
||||
<cfhttpparam type="file" name="img" file="#expandPath('/taffy/tests/tests/upload.png')#" />
|
||||
</cfhttp>
|
||||
<!--- <cfset debug(local.uploadResult) /> --->
|
||||
<cfset assertTrue(local.uploadResult.statusCode eq "200 OK", "Did not return status 200") />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="test_throws_exception_when_ressource_uri_doesnt_begin_with_forward_slash">
|
||||
<cfset assertTrue(checkIfOneSkippedRessourceContainsExpectedException("detail", "The URI (uriWithoutForwardSlash) for `uriDoesntBeginWithForwardSlash` should begin with a forward slash."), "Uri without forward slash not showing in errors")>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="test_throws_exception_when_alias_ressource_uri_doesnt_begin_with_forward_slash">
|
||||
<cfset assertTrue(checkIfOneSkippedRessourceContainsExpectedException("detail", "The URI (uriAliasWithoutFowardSlash) for `uriAliasDoesntBeginWithForwardSlash` should begin with a forward slash."), "Uri alias without forward slash not showing in errors")>
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,94 +0,0 @@
|
||||
component extends="base" {
|
||||
|
||||
|
||||
function setup(){
|
||||
reloadFramework();
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
reloadFramework();
|
||||
}
|
||||
|
||||
function beforeTests(){
|
||||
|
||||
}
|
||||
|
||||
|
||||
function test_loads_directory_named_resources() {
|
||||
// Setup external factory to read resource directory. Ignore resources that were designed to cause errors.
|
||||
local.config = {
|
||||
exclude = ["uriDoesnt","uriAliasDoesnt","conflict"]
|
||||
};
|
||||
|
||||
local.beanFactory = createObject("component", "di1.ioc").init(["/taffy/tests/resources"], local.config);
|
||||
|
||||
|
||||
// Setup Taffy with new bean factory to use and call 'onApplicationStart' to properly initialize.
|
||||
local.taffy = createObject("component","taffy.tests.Application");
|
||||
|
||||
makePublic(local.taffy, "getExternalBeanFactory");
|
||||
makePublic(local.taffy, "getBeanListFromExternalFactory");
|
||||
|
||||
injectMethod(local.taffy, this, "_getVariables", "getVariables");
|
||||
|
||||
local.vars = local.taffy.getVariables();
|
||||
local.vars.framework.beanFactory = local.beanFactory;
|
||||
|
||||
local.taffy.onApplicationStart();
|
||||
|
||||
|
||||
// Grab the resource list and compare.
|
||||
local.resourceList = local.taffy.getBeanListFromExternalFactory(local.taffy.getExternalBeanFactory());
|
||||
|
||||
structDelete(local.vars.framework, "beanFactory");
|
||||
|
||||
local.taffy.onApplicationStart();
|
||||
|
||||
debug("Available resources from external factory: '" & replace(local.resourceList, ",", ", ", "all") & "'");
|
||||
assert(listLen(local.resourceList) gt 0, "No resources were loaded from the external factory.");
|
||||
}
|
||||
|
||||
|
||||
function test_loads_explicit_named_resources() {
|
||||
// Setup external factory to read resource directory. Ignore resources that were designed to cause errors.
|
||||
local.config = {
|
||||
exclude = ["uriDoesnt","uriAliasDoesnt","conflict"],
|
||||
omitDirectoryAliases = true
|
||||
};
|
||||
|
||||
local.beanFactory = createObject("component", "di1.ioc").init(["/taffy/tests/resources"], local.config);
|
||||
|
||||
|
||||
// Setup Taffy with new bean factory to use and call 'onApplicationStart' to properly initialize.
|
||||
local.taffy = createObject("component","taffy.tests.Application");
|
||||
|
||||
makePublic(local.taffy, "getExternalBeanFactory");
|
||||
makePublic(local.taffy, "getBeanListFromExternalFactory");
|
||||
|
||||
injectMethod(local.taffy, this, "_getVariables", "getVariables");
|
||||
|
||||
local.vars = local.taffy.getVariables();
|
||||
|
||||
local.vars.framework.beanFactory = local.beanFactory;
|
||||
|
||||
local.taffy.onApplicationStart();
|
||||
|
||||
|
||||
// Grab the resource list and compare.
|
||||
debug(local.beanFactory.getBeanInfo());
|
||||
local.resourceList = local.taffy.getBeanListFromExternalFactory(local.taffy.getExternalBeanFactory());
|
||||
|
||||
structDelete(local.vars.framework, "beanFactory");
|
||||
local.taffy.onApplicationStart();
|
||||
|
||||
debug("Available resources from external factory: '" & replace(local.resourceList, ",", ", ", "all") & "'");
|
||||
|
||||
//THIS TEST CURRENTLY FAILING DUE TO https://github.com/atuttle/Taffy/issues/276
|
||||
//assert(listLen(local.resourceList) gt 0, "No resources were loaded from the external factory.");
|
||||
}
|
||||
|
||||
|
||||
private function _getVariables(){
|
||||
return variables;
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
<cfcomponent extends="base">
|
||||
<cfscript>
|
||||
|
||||
function setup() {
|
||||
reloadFramework();
|
||||
}
|
||||
|
||||
|
||||
function beforeTests(){
|
||||
request["_testsRunning"] = true;
|
||||
variables.taffy = createObject("component","taffy.tests.Application");
|
||||
makePublic(variables.taffy, "getBeanFactory");
|
||||
variables.factory = variables.taffy.getBeanFactory();
|
||||
//debug(variables.factory);
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resources'), 'taffy.tests.resources', expandPath('/taffy/tests/resources'), true, variables.taffy);
|
||||
}
|
||||
|
||||
function test_throws_on_getBean_not_exists(){
|
||||
var local = {};
|
||||
local.nonExistentBean = "does_not_exist";
|
||||
|
||||
try{
|
||||
local.result = variables.factory.getBean(local.nonExistentBean);
|
||||
fail("Expected 'Bean Not Found' exception to be thrown, but none was.");
|
||||
} catch (Taffy.Factory.BeanNotFound e) {
|
||||
//debug(e);
|
||||
assertTrue(findNoCase('not found', e.message) gt 0, "TaffyFactory exception message did not contain the words 'not found'.");
|
||||
}
|
||||
}
|
||||
|
||||
function test_autowires_properties_in_beans(){
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resources'), 'taffy.tests.resources', expandPath('/taffy/tests/resources'), true );
|
||||
local.bean = variables.factory.getBean( 'EchoMember' );
|
||||
// debug(local.bean);
|
||||
assertTrue( structKeyExists(local.bean, "dependency1") );
|
||||
assertFalse( isSimpleValue(local.bean.dependency1) );
|
||||
}
|
||||
|
||||
function test_autowires_setters_in_beans(){
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resources'), 'taffy.tests.resources', expandPath('/taffy/tests/resources'), true );
|
||||
local.bean = variables.factory.getBean( 'EchoRegexMember' );
|
||||
// debug(local.bean);
|
||||
assertTrue( structKeyExists(local.bean, "dependency2") );
|
||||
assertFalse( isSimpleValue(local.bean.dependency2) );
|
||||
}
|
||||
|
||||
function test_autowires_properties_in_transients(){
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resources'), 'taffy.tests.resources', expandPath('/taffy/tests/resources'), true );
|
||||
local.bean = variables.factory.getBean( 'CustomJsonSerializer' );
|
||||
// debug(local.bean);
|
||||
assertTrue( structKeyExists(local.bean, "dependency1") );
|
||||
assertFalse( isSimpleValue(local.bean.dependency1) );
|
||||
}
|
||||
|
||||
function test_autowires_setters_in_transients(){
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resources'), 'taffy.tests.resources', expandPath('/taffy/tests/resources'), true );
|
||||
local.bean = variables.factory.getBean( 'CustomJsonSerializer' );
|
||||
// debug(local.bean);
|
||||
assertTrue( structKeyExists(local.bean, "dependency2") );
|
||||
assertFalse( isSimpleValue(local.bean.dependency2) );
|
||||
}
|
||||
|
||||
function test_skips_resources_with_errors(){
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resourcesError'), 'taffy.tests.resourcesError', expandPath('/taffy/tests/resourcesError'), true );
|
||||
// debug(variables.taffy.status);
|
||||
assertTrue(structKeyExists(variables.taffy.status, "skippedResources"));
|
||||
}
|
||||
|
||||
function test_lists_skipped_resources(){
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resourcesError'), 'taffy.tests.resourcesError', expandPath('/taffy/tests/resourcesError'), true, variables.taffy);
|
||||
debug(variables.taffy.status);
|
||||
assertTrue(structKeyExists(variables.taffy.status, "skippedResources"));
|
||||
assertTrue( arrayLen(variables.taffy.status.skippedResources) gt 0 );
|
||||
}
|
||||
|
||||
function test_clears_skipped_resources_on_reload(){
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resourcesError'), 'taffy.tests.resourcesError', expandPath('/taffy/tests/resourcesError'), true, variables.taffy );
|
||||
// debug(variables.taffy.status);
|
||||
assertTrue(structKeyExists(variables.taffy.status, "skippedResources"));
|
||||
assertTrue( arrayLen(variables.taffy.status.skippedResources) gt 0 );
|
||||
|
||||
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resources'), 'taffy.tests.resources', expandPath('/taffy/tests/resources'), true, variables.taffy );
|
||||
// debug(variables.taffy.status);
|
||||
assertTrue(structKeyExists(variables.taffy.status, "skippedResources"));
|
||||
assertTrue( ArrayLen(variables.taffy.status.skippedResources) eq 0, "Expected skipped resources array to be empty but it wasn't" );
|
||||
}
|
||||
|
||||
function test_treats_CRCs_as_transients(){
|
||||
var local = {};
|
||||
//this resource+method explicitly sets response status of 999
|
||||
local.result = apiCall("get", "/echo/2.json", "foo=bar");
|
||||
// debug(local.result);
|
||||
assertEquals(999, local.result.responseHeader.status_code);
|
||||
//this resource+method usees the default response status => 200=passing test, 999=failing test
|
||||
local.result = apiCall("get", "/echo/tunnel/2.json", "foo=bar");
|
||||
// debug(local.result);
|
||||
assertEquals(200, local.result.responseHeader.status_code);
|
||||
}
|
||||
|
||||
</cfscript>
|
||||
</cfcomponent>
|
||||
@@ -1,63 +0,0 @@
|
||||
<cfcomponent extends="base">
|
||||
<cfscript>
|
||||
|
||||
function test_hoth(){
|
||||
var mockHoth = getMockBox().createMock(className="Hoth.HothTracker", callLogging=true);
|
||||
var hothAdapter = '';
|
||||
var fakeError = {};
|
||||
var callLog = {};
|
||||
//setup the behaviors we're expecting the adapter to run
|
||||
//mockHoth.track('{struct}');
|
||||
mockHoth.$('track', true);
|
||||
//create hoth adapter to test
|
||||
hothAdapter = createObject("component", "taffy.bonus.LogToHoth").init(
|
||||
"Taffy.examples.api_Hoth.resources.HothConfig",
|
||||
mockHoth
|
||||
);
|
||||
|
||||
//fake error
|
||||
fakeError.message = "This is a test error";
|
||||
fakeError.detail = "Rubber Baby Buggy Bumper";
|
||||
hothAdapter.saveLog(fakeError);
|
||||
|
||||
callLog = mockHoth.$callLog();
|
||||
debug(callLog);
|
||||
assertTrue(structKeyExists(callLog, "track"), "should call track method");
|
||||
assertTrue(isStruct(callLog.track[1][1]), "args should be a struct");
|
||||
assertEquals(callLog.track[1][1].message, fakeError.message);
|
||||
|
||||
}
|
||||
|
||||
function test_BugLogHQ(){
|
||||
var blhqSettings = { bugLogListener = "http://#cgi.server_name#:#cgi.server_port#/tests/BugLogHQ/listeners/bugLogListenerREST.cfm" };
|
||||
var mockBLHQ = new bugLog.client.bugLogService(argumentCollection=blhqSettings);
|
||||
var blhqAdapter = '';
|
||||
var fakeError = {};
|
||||
fakeError.message = "This is a test error";
|
||||
fakeError.detail = "Rubber Baby Buggy Bumper";
|
||||
|
||||
if (structKeyExists(server, "coldfusion") && structKeyExists(server.coldfusion, "productversion")) {
|
||||
if (val(listFirst(server.coldfusion.productversion)) < 11) {
|
||||
//datasource definition will only work on CF11+
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mockBLHQ.init(argumentCollection=blhqSettings);
|
||||
|
||||
blhqAdapter = createObject("component", "taffy.bonus.LogToBugLogHQ").init(
|
||||
config=blhqSettings,
|
||||
tracker=mockBLHQ
|
||||
);
|
||||
|
||||
//fake error
|
||||
fakeError.message = "This is a test error";
|
||||
fakeError.detail = "Rubber Baby Buggy Bumper";
|
||||
blhqAdapter.saveLog(fakeError);
|
||||
|
||||
|
||||
}
|
||||
|
||||
</cfscript>
|
||||
</cfcomponent>
|
||||
@@ -1,40 +0,0 @@
|
||||
<cfcomponent extends="base">
|
||||
<cfscript>
|
||||
function setup(){
|
||||
variables.serializer = createObject("component", "taffy.core.baseSerializer");
|
||||
}
|
||||
|
||||
function test_setData_getData(){
|
||||
variables.serializer.setData(10);
|
||||
assertEquals(10, variables.serializer.getData());
|
||||
}
|
||||
|
||||
function test_noData_returns_empty_rep_obj(){
|
||||
local.result = variables.serializer.noData();
|
||||
local.meta = getMetaData(local.result);
|
||||
// debug(local.meta);
|
||||
// debug(local.result.getData());
|
||||
assertEquals('taffy.core.baseSerializer', local.meta.fullname);
|
||||
assertEquals("", local.result.getData());
|
||||
}
|
||||
|
||||
function test_withStatus_getStatus(){
|
||||
variables.serializer.withStatus(42);
|
||||
assertEquals(42, variables.serializer.getStatus());
|
||||
}
|
||||
|
||||
function test_withStatus_getStatusText(){
|
||||
variables.serializer.withStatus(404, "Not Found");
|
||||
assertEquals("Not Found", variables.serializer.getStatusText());
|
||||
variables.serializer.withStatus(418);
|
||||
assertEquals("I'm a teapot", variables.serializer.getStatusText());
|
||||
}
|
||||
|
||||
function test_withHeaders_getHeaders(){
|
||||
local.h = {};
|
||||
local.h['x-dude'] = 'dude!';
|
||||
variables.serializer.withHeaders(local.h);
|
||||
assertEquals(true, structKeyExists(variables.serializer.getHeaders(), "x-dude"));
|
||||
}
|
||||
</cfscript>
|
||||
</cfcomponent>
|
||||
@@ -1,50 +0,0 @@
|
||||
component extends="base" {
|
||||
|
||||
|
||||
|
||||
function beforeTests(){
|
||||
variables.taffy = createObject("component","taffy.tests.Application");
|
||||
variables.resource = createObject("component", "taffy.core.resource");
|
||||
}
|
||||
|
||||
function test_representationOf_returns_repClass(){
|
||||
application._taffy.settings.serializer = "taffy.core.nativeJsonSerializer";
|
||||
makePublic(variables.resource, "representationOf");
|
||||
local.result = variables.resource.representationOf(10);
|
||||
local.meta = getMetadata(local.result);
|
||||
// debug(local.meta);
|
||||
assertEquals(true, eventuallyInherits(local.meta, 'taffy.core.baseSerializer'));
|
||||
}
|
||||
|
||||
function test_queryToArray_respects_column_case(){
|
||||
makePublic(variables.resource, "queryToArray");
|
||||
local.before = QueryNew("Foo,Bar");
|
||||
queryAddRow(local.before);
|
||||
querySetCell(local.before, "Foo", 42, 1);
|
||||
querySetCell(local.before, "Bar", "fubar", 1);
|
||||
// debug(local.before);
|
||||
local.after = variables.resource.queryToArray(local.before);
|
||||
// debug(local.after);
|
||||
local.keyList = structKeyList(local.after[1]);
|
||||
local.serialized = serializeJSON(local.after);
|
||||
// debug(local.serialized);
|
||||
|
||||
assertTrue( (local.keyList eq "Foo,Bar" or local.keyList eq "Bar,Foo"), 'column name case is not as expected');
|
||||
assertTrue( (local.serialized eq '[{"Foo":42,"Bar":"fubar"}]') or (local.serialized eq '[{"Bar":"fubar","Foo":42}]') );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* recursive method used to check entire inheritance tree to find that a certain parent class exists somewhere within it */
|
||||
private boolean function eventuallyInherits(required struct md, required string class) {
|
||||
if (structKeyExists(md, "fullname") && md.fullname eq class) {
|
||||
return true;
|
||||
} else {
|
||||
if (structKeyExists(md, "extends")) {
|
||||
return eventuallyInherits(md.extends, class);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<cfcomponent extends="mxunit.framework.TestCase">
|
||||
|
||||
<cffunction name="apiCall" access="private" output="false">
|
||||
<cfargument name="method" type="string"/>
|
||||
<cfargument name="uri" type="string"/>
|
||||
<cfargument name="query" type="string"/>
|
||||
<cfargument name="headers" type="struct" default="#structNew()#" />
|
||||
<cfargument name="basicauth" type="string" default="" />
|
||||
|
||||
<cfset var local = structNew() />
|
||||
<cfset local.apiRootURL = getDirectoryFromPath(cgi.script_name) />
|
||||
<cfset local.apiRootURL = listDeleteAt(local.apiRootURL,listLen(local.apiRootURL,'/'),'/') />
|
||||
|
||||
<cfset local.args = {} />
|
||||
<cfset local.args.method = arguments.method />
|
||||
<cfset local.args.redirect = false />
|
||||
<cfset local.args.result = "local.result" />
|
||||
<cfset local.args.charset = "utf-8" />
|
||||
<cfif len(arguments.basicauth)>
|
||||
<cfset local.args.username = listFirst(arguments.basicauth, ":") />
|
||||
<cfset local.args.password = listRest(arguments.basicAuth, ":") />
|
||||
</cfif>
|
||||
<cfset local.args.url = "http://#CGI.SERVER_NAME#:#CGI.SERVER_PORT##local.apiRootURL#/index.cfm#arguments.uri#" />
|
||||
|
||||
<cfif lcase(arguments.method) eq "put" or lcase(arguments.method) eq "post">
|
||||
<cfhttp attributeCollection="#local.args#">
|
||||
<cfif isJson(query)>
|
||||
<cfhttpparam type="header" name="Content-Type" value="text/json" />
|
||||
<cfelse>
|
||||
<cfhttpparam type="header" name="Content-Type" value="application/x-www-form-urlencoded" />
|
||||
</cfif>
|
||||
<cfhttpparam type="body" value="#arguments.query#" />
|
||||
|
||||
<!--- Add arbitrary headers to request --->
|
||||
<cfloop item="local.header" collection="#arguments.headers#">
|
||||
<cfhttpparam type="header" name="#local.header#" value="#arguments.headers[local.header]#" />
|
||||
</cfloop>
|
||||
</cfhttp>
|
||||
<cfelse>
|
||||
<cfif find('?',local.args.url)>
|
||||
<cfset local.args.url = '#local.args.url##arguments.query#' />
|
||||
<cfelse>
|
||||
<cfset local.args.url = '#local.args.url#?#arguments.query#' />
|
||||
</cfif>
|
||||
<cfhttp attributeCollection="#local.args#">
|
||||
<!--- Add arbitrary headers to request --->
|
||||
<cfloop item="local.header" collection="#arguments.headers#">
|
||||
<cfhttpparam type="header" name="#local.header#" value="#arguments.headers[local.header]#" />
|
||||
</cfloop>
|
||||
</cfhttp>
|
||||
</cfif>
|
||||
<cfreturn local.result />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="getUrl" access="private" output="false">
|
||||
<cfargument name="url" type="string" required="true" />
|
||||
<cfhttp method="get" url="#arguments.url#" result="local.result" />
|
||||
<cfreturn local.result />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="checkIfOneSkippedRessourceContainsExpectedException" access="private" output="false" returntype="boolean">
|
||||
<cfargument name="exceptionAttributeName" type="string" required="true" />
|
||||
<cfargument name="expectedValue" type="string" required="true" />
|
||||
<cfset var skippedRessource = "">
|
||||
<cfset var expectedExceptionAttributeValueFound = 0>
|
||||
|
||||
<cfloop array="#application._taffy.status.skippedResources#" index="skippedRessource">
|
||||
<cfset expectedExceptionAttributeValueFound = structKeyExists(skippedRessource.exception, arguments.exceptionAttributeName) and skippedRessource.exception[arguments.exceptionAttributeName] is arguments.expectedValue>
|
||||
<cfif expectedExceptionAttributeValueFound>
|
||||
<cfbreak>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<cfreturn expectedExceptionAttributeValueFound>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="reloadFramework" access="private">
|
||||
<cfset local.apiRootURL = getDirectoryFromPath(cgi.script_name) />
|
||||
<cfset local.apiRootURL = listDeleteAt(local.apiRootURL,listLen(local.apiRootURL,'/'),'/') />
|
||||
<cfhttp method="GET" url="http://#CGI.SERVER_NAME#:#CGI.SERVER_PORT##local.apiRootURL#/index.cfm?#application._taffy.settings.reloadkey#=#application._taffy.settings.reloadPassword#" />
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -1,10 +0,0 @@
|
||||
<cfsetting requesttimeout="600" />
|
||||
|
||||
<cfinvoke component="mxunit.runner.DirectoryTestSuite"
|
||||
method="run"
|
||||
directory="#expandPath('/Taffy/tests/tests')#"
|
||||
componentPath="Taffy.tests.tests"
|
||||
recurse="true"
|
||||
returnvariable="results" />
|
||||
|
||||
<cfoutput> #results.getResultsOutput('extjs')# </cfoutput>
|
||||
@@ -1,19 +0,0 @@
|
||||
<cfsetting showdebugoutput="false" />
|
||||
<cfscript>
|
||||
testsDir = expandPath('/Taffy/tests/tests');
|
||||
componentPath = 'Taffy.tests.tests';
|
||||
|
||||
suite = createObject('mxunit.framework.TestSuite');
|
||||
files = createObject('mxunit.runner.DirectoryTestSuite').getTests(directory=testsDir, componentPath=componentPath);
|
||||
|
||||
for (i = 1; i <= arrayLen(files); i++){
|
||||
suite.addAll(files[i]);
|
||||
}
|
||||
|
||||
tests = suite.suites();
|
||||
|
||||
for (t in tests){
|
||||
tests[t] = tests[t].methods;
|
||||
}
|
||||
</cfscript>
|
||||
<cfcontent reset="true" type="application/json; charset=UTF-8" /><cfoutput>#serializeJson(tests)#</cfoutput><cfabort/>
|
||||
@@ -1,46 +0,0 @@
|
||||
<cfparam name="url.reporter" default="simple" type="variablename">
|
||||
<cfset plainText = url.reporter IS "text" OR url.reporter IS "mintext">
|
||||
<cfsetting requesttimeout="150" enablecfoutputonly="#plainText#">
|
||||
<cfif plainText>
|
||||
<cfcontent type="text/plain">
|
||||
</cfif>
|
||||
|
||||
<strong>Test Environment</strong>:
|
||||
<cfif server.coldfusion.productname contains "ColdFusion">
|
||||
<cfoutput>Adobe ColdFusion #server.coldfusion.productversion#</cfoutput>
|
||||
<cfelseif structKeyExists(server, "lucee")>
|
||||
<cfoutput>Lucee #server.lucee.version#</cfoutput>
|
||||
<cfelseif structKeyExists(server, "railo")>
|
||||
<cfoutput>Railo #server.railo.version#</cfoutput>
|
||||
<cfelse>
|
||||
<cfoutput>#server.coldfusion.productname#</cfoutput>
|
||||
</cfif>
|
||||
<cfoutput> on #server.os.name# #server.os.version#</cfoutput>
|
||||
<cfset system = createObject("java", "java.lang.System")>
|
||||
<cfoutput> running Java #system.getProperty("java.version")#
|
||||
|
||||
</cfoutput>
|
||||
|
||||
|
||||
<cfset bundles=[]>
|
||||
<cfdirectory action="list" directory="#getDirectoryFromPath(getCurrentTemplatePath())#" name="files">
|
||||
<cfloop query="files">
|
||||
<cfif left(getFileFromPath(files.name), 4) IS "Test">
|
||||
<cfset arrayAppend(bundles, "Taffy.tests.tests." & replace(getFileFromPath(files.name), ".cfc", ""))>
|
||||
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<cfset r = new testbox.system.TestBox(reporter=url.reporter, bundles=bundles)>
|
||||
<cfsavecontent variable="resultOutput"><cfoutput>#r.run()#</cfoutput></cfsavecontent>
|
||||
<cfoutput>#trim(resultOutput)#</cfoutput>
|
||||
<cfset resultObject = r.getResult()>
|
||||
<cfset errors = resultObject.getTotalFail() + resultObject.getTotalError()>
|
||||
<cfif errors GT 0>
|
||||
<cfheader statuscode="500" statustext="Tests Failed">
|
||||
</cfif>
|
||||
|
||||
<cfif NOT plainText>
|
||||
<cfoutput>
|
||||
<a href="run.cfm?ts=#GetTickCount()#">ReRun</a>
|
||||
</cfoutput>
|
||||
</cfif>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB |
Reference in New Issue
Block a user