TimeTrex/unit_tests/testcases/other/SQLTest.php

1153 lines
39 KiB
PHP

<?php /** @noinspection PhpMissingDocCommentInspection */
/*********************************************************************************
*
* TimeTrex is a Workforce Management program developed by
* TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
*
* You should have received a copy of the GNU Affero General Public License along
* with this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
*
* You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
* #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
*
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by TimeTrex" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by TimeTrex".
*
********************************************************************************/
class SQLTest extends PHPUnit\Framework\TestCase {
public function setUp(): void {
global $dd;
Debug::text( 'Running setUp(): ', __FILE__, __LINE__, __METHOD__, 10 );
$dd = new DemoData();
$dd->setEnableQuickPunch( false ); //Helps prevent duplicate punch IDs and validation failures.
$dd->setUserNamePostFix( '_' . uniqid( null, true ) ); //Needs to be super random to prevent conflicts and random failing tests.
$this->company_id = $dd->createCompany();
$this->legal_entity_id = $dd->createLegalEntity( $this->company_id, 10 );
Debug::text( 'Company ID: ' . $this->company_id, __FILE__, __LINE__, __METHOD__, 10 );
$this->assertTrue( TTUUID::isUUID( $this->company_id ) );
//$dd->createPermissionGroups( $this->company_id, 40 ); //Administrator only.
$dd->createCurrency( $this->company_id, 10 );
$this->branch_id = $dd->createBranch( $this->company_id, 10 ); //NY
//$dd->createPayStubAccount( $this->company_id );
//$dd->createPayStubAccountLink( $this->company_id );
$dd->createUserWageGroups( $this->company_id );
$this->user_id = $dd->createUser( $this->company_id, $this->legal_entity_id, 100 );
$this->assertTrue( TTUUID::isUUID( $this->user_id ) );
}
public function tearDown(): void {
Debug::text( 'Running tearDown(): ', __FILE__, __LINE__, __METHOD__, 10 );
}
function getListFactoryClassList( $equal_parts = 1 ) {
global $global_class_map;
$retarr = [];
//Get all ListFactory classes
foreach ( $global_class_map as $class_name => $class_file_name ) {
if ( strpos( $class_name, 'ListFactory' ) !== false ) {
$retarr[] = $class_name;
}
}
$chunk_size = ceil( ( count( $retarr ) / $equal_parts ) );
return array_chunk( $retarr, $chunk_size );
}
/** @noinspection PhpMissingBreakStatementInspection */
function runSQLTestOnListFactory( $factory_name ) {
if ( class_exists( $factory_name ) ) {
$reflectionClass = new ReflectionClass( $factory_name );
$class_file_name = $reflectionClass->getFileName();
Debug::text( 'Checking Class: ' . $factory_name . ' File: ' . $class_file_name, __FILE__, __LINE__, __METHOD__, 10 );
$filter_data_types = [
'not_set',
'true',
'false',
'null',
'empty_string',
'negative_small_int',
'small_int',
'large_int',
'string',
'array',
];
//Parse filter array keys from class file so we can populate them with dummy data.
preg_match_all( '/\$filter_data\[\'([a-z0-9_]*)\'\]/i', file_get_contents( $class_file_name ), $filter_data_match );
if ( isset( $filter_data_match[1] ) ) {
//Debug::Arr($filter_data_match, 'Filter Data Match: ', __FILE__, __LINE__, __METHOD__, 10);
foreach ( $filter_data_types as $filter_data_type ) {
Debug::Text( 'Filter Data Type: ' . $filter_data_type, __FILE__, __LINE__, __METHOD__, 10 );
$filter_data = [];
$filter_data_match[1] = array_unique( $filter_data_match[1] );
foreach ( $filter_data_match[1] as $filter_data_key ) {
//Skip sort_column/sort_order
if ( in_array( $filter_data_key, [ 'sort_column', 'sort_order' ] ) ) {
continue;
}
//Test with:
// Small Integers
// Large Integers (64bit)
// Strings
// Arrays
switch ( $filter_data_type ) {
case 'true':
$filter_data[$filter_data_key] = true;
break;
case 'false':
$filter_data[$filter_data_key] = false;
break;
case 'null':
$filter_data[$filter_data_key] = null;
break;
case 'empty_string':
$filter_data[$filter_data_key] = '';
break;
case 'negative_small_int':
$filter_data[$filter_data_key] = ( rand( 0, 128 ) * -1 );
break;
case 'small_int':
$filter_data[$filter_data_key] = rand( 0, 128 );
break;
case 'large_int':
$filter_data[$filter_data_key] = rand( 2147483648, 21474836489 );
break;
case 'string':
$filter_data[$filter_data_key] = 'A' . substr( md5( microtime() ), rand( 0, 26 ), 10 );
break;
case 'array':
$filter_data[$filter_data_key] = [ rand( 0, 128 ), rand( 2147483648, 21474836489 ), 'A' . substr( md5( microtime() ), rand( 0, 26 ), 10 ) ];
break;
case 'not_set':
break;
}
}
//Debug::Arr($filter_data, 'Filter Data: ', __FILE__, __LINE__, __METHOD__, 10);
$lf = TTNew( $factory_name );
switch ( $factory_name ) {
case 'ScheduleListFactory':
$retarr = $lf->getSearchByCompanyIdAndArrayCriteria( $this->company_id, $filter_data, 1, 1, null, null );
$this->assertNotEquals( false, $retarr );
$this->assertTrue( is_object( $retarr ), true );
$retarr = $lf->getAPISearchByCompanyIdAndArrayCriteria( $this->company_id, $filter_data, 1, 1, null, null );
$this->assertNotEquals( false, $retarr );
$this->assertTrue( is_object( $retarr ), true );
break;
case 'MessageControlListFactory':
case 'NotificationListFactory':
$filter_data['current_user_id'] = $this->user_id;
//break; //Intentional that break is not here so it spills into default?
default:
if ( method_exists( $lf, 'getAPISearchByCompanyIdAndArrayCriteria' ) ) {
//Make sure we test pagination, especially with subqueries and the need for _ADODB_COUNT workarounds, $limit = NULL, $page = NULL, $where = NULL, $order = NULL
$retarr = $lf->getAPISearchByCompanyIdAndArrayCriteria( $this->company_id, $filter_data, 1, 1, null, null );
$this->assertNotEquals( false, $retarr );
$this->assertTrue( is_object( $retarr ), true );
}
break;
}
}
}
unset( $filter_data_match );
Debug::text( 'Done...', __FILE__, __LINE__, __METHOD__, 10 );
return true;
} else {
Debug::text( 'Class does not exist: ' . $factory_name, __FILE__, __LINE__, __METHOD__, 10 );
}
return false;
}
function runSQLTestOnListFactoryMethods( $factory_name ) {
if ( in_array( $factory_name, [
'HierarchyListFactory',
'PolicyGroupAccrualPolicyListFactory',
'PolicyGroupOverTimePolicyListFactory',
'PolicyGroupPremiumPolicyListFactory',
'PolicyGroupRoundIntervalPolicyListFactory',
'ProductTaxPolicyProductListFactory',
'RecurringScheduleUserListFactory',
]
)
) {
return true; //Deprecated classes.
}
if ( class_exists( $factory_name ) ) {
$reflectionClass = new ReflectionClass( $factory_name );
Debug::text( 'Checking Class: ' . $factory_name, __FILE__, __LINE__, __METHOD__, 10 );
$raw_methods = $reflectionClass->getMethods( ReflectionMethod::IS_PUBLIC );
if ( is_array( $raw_methods ) ) {
foreach ( $raw_methods as $raw_method ) {
if ( $factory_name == $raw_method->class
&& (
strpos( $raw_method->name, 'getAll' ) !== false
|| strpos( $raw_method->name, 'getBy' ) !== false
|| strpos( $raw_method->name, 'Report' ) !== false
)
&& (
//Skip getByCompanyIdArray() functions, but include getBy*AndArrayCriteria(). So just check if its ends with Array or not.
( substr( $raw_method->name, -5 ) !== 'Array' )
)
) {
Debug::text( 'Class: ' . $factory_name . ' Method: ' . $raw_method->name, __FILE__, __LINE__, __METHOD__, 10 );
$test_modes = [ 'default', 'fuzz' ];
foreach ( $test_modes as $test_mode ) {
Debug::text( ' Test Mode: ' . $test_mode, __FILE__, __LINE__, __METHOD__, 10 );
//Get method arguments.
$method_parameters = $raw_method->getParameters();
if ( is_array( $method_parameters ) ) {
$input_arguments = [];
foreach ( $method_parameters as $method_parameter ) {
Debug::text( ' Parameter: ' . $method_parameter->name, __FILE__, __LINE__, __METHOD__, 10 );
switch ( $factory_name ) {
case 'ClientContactListFactory':
switch ( $method_parameter->name ) {
case 'key':
$input_argument = '900d6136975e3a728051a62ed119191034568745';
break;
case 'name':
$input_argument = 'test';
break;
}
break;
case 'RoundIntervalPolicyListFactory':
switch ( $method_parameter->name ) {
case 'type_id':
$input_argument = 40;
break;
}
break;
case 'ScheduleListFactory':
switch ( $method_parameter->name ) {
case 'direction':
$input_argument = 'before';
break;
}
break;
case 'UserListFactory':
case 'UserContactListFactory':
case 'JobApplicantListFactory':
switch ( $method_parameter->name ) {
case 'email':
$input_argument = 'hi@hi.com';
break;
case 'key':
$input_argument = '900d6136975e3a728051a62ed119191034568745';
break;
}
break;
case 'PayPeriodTimeSheetVerifyListFactory':
case 'RequestListFactory':
switch ( $method_parameter->name ) {
case 'hierarchy_level_map':
$input_argument = [
[
'hierarchy_control_id' => 1,
'level' => 1,
'last_level' => 2,
'object_type_id' => 10,
],
];
break;
}
break;
case 'ExceptionListFactory':
switch ( $method_parameter->name ) {
case 'time_period':
$input_argument = 'week';
break;
}
break;
}
if ( $test_mode == 'fuzz' ) {
//If LIMIT argument is available always set it to 1 to reduce memory usage.
if ( in_array( $method_parameter->name, [ 'where', 'order', 'page' ] ) ) {
$input_argument = null;
} else if ( !isset( $input_argument ) && ( $method_parameter->name == 'id' || strpos( $method_parameter->name, '_id' ) !== false || $method_parameter->name == 'limit' ) ) { //Use integer as its a ID argument.
$input_argument = 'false'; //Try passing a string where ID is expected.
} else if ( !isset( $input_argument ) ) {
$input_argument = 2;
}
$input_arguments[] = $input_argument;
} else {
//If LIMIT argument is available always set it to 1 to reduce memory usage.
if ( in_array( $method_parameter->name, [ 'where', 'order', 'page' ] ) ) {
$input_argument = null;
} else if ( !isset( $input_argument ) && ( $method_parameter->name == 'id' || strpos( $method_parameter->name, '_id' ) !== false || $method_parameter->name == 'limit' ) ) { //Use integer as its a ID argument.
$input_argument = 1;
} else if ( !isset( $input_argument ) ) {
$input_argument = 2;
}
$input_arguments[] = $input_argument;
}
unset( $input_argument );
}
if ( isset( $input_arguments ) && is_array( $input_arguments ) ) {
Debug::Arr( $input_arguments, ' Calling Class: ' . $factory_name . ' Method: ' . $raw_method->name, __FILE__, __LINE__, __METHOD__, 10 );
$lf = TTNew( $factory_name );
switch ( $factory_name . '::' . $raw_method->name ) {
case 'StationListFactory::getByUserIdAndStatusAndType':
case 'PayStubEntryAccountListFactory::getByTypeArrayByCompanyIdAndStatusId':
//Skip due to failures.
break;
case 'CompanyListFactory::getByPhoneID':
$retarr = call_user_func_array( [ $lf, $raw_method->name ], $input_arguments );
if ( $test_mode == 'fuzz' ) {
$this->assertEquals( false, ( ( is_object( $retarr ) ) ? ( ( $retarr->getRecordCount() == 0 ) ? false : true ) : $retarr ) ); //This will be FALSE
} else {
$this->assertNotEquals( false, $retarr );
$this->assertTrue( is_object( $retarr ), true );
}
break;
case 'MessageControlListFactory::getByCompanyIdAndObjectTypeAndObjectAndNotUser':
$retarr = call_user_func_array( [ $lf, $raw_method->name ], $input_arguments );
$this->assertEquals( false, ( ( is_object( $retarr ) ) ? ( ( $retarr->getRecordCount() == 0 ) ? false : true ) : $retarr ) ); //This will be FALSE, but it still executes a query.
//$this->assertTrue( is_object($retarr), TRUE );
break;
case 'PayStubEntryListFactory::getByPayStubIdAndEntryNameId':
//FUZZ tests should return FALSE, otherwise they should be normal.
$retarr = call_user_func_array( [ $lf, $raw_method->name ], $input_arguments );
if ( $test_mode == 'fuzz' ) {
$this->assertEquals( false, ( ( is_object( $retarr ) ) ? ( ( $retarr->getRecordCount() == 0 ) ? false : true ) : $retarr ) ); //This will be FALSE
} else {
$this->assertNotEquals( false, $retarr );
$this->assertTrue( is_object( $retarr ), true );
}
break;
default:
$retarr = call_user_func_array( [ $lf, $raw_method->name ], $input_arguments );
//Debug::Arr($retarr, ' RetArr: ', __FILE__, __LINE__, __METHOD__, 10);
$this->assertNotEquals( false, $retarr );
$this->assertTrue( is_object( $retarr ), true );
break;
}
} else {
Debug::text( ' No INPUT arguments... Skipping Class: ' . $factory_name . ' Method: ' . $raw_method->name, __FILE__, __LINE__, __METHOD__, 10 );
}
}
}
} else {
Debug::text( 'Skipping... Class: ' . $factory_name . ' Method: ' . $raw_method->name, __FILE__, __LINE__, __METHOD__, 10 );
}
}
}
Debug::text( 'Done...', __FILE__, __LINE__, __METHOD__, 10 );
return true;
} else {
Debug::text( 'Class does not exist: ' . $factory_name, __FILE__, __LINE__, __METHOD__, 10 );
}
return false;
}
function runSQLSortTestOnListFactory( $factory_name ) {
if ( class_exists( $factory_name ) ) {
$reflectionClass = new ReflectionClass( $factory_name );
$class_file_name = $reflectionClass->getFileName();
Debug::text( 'Checking Class: ' . $factory_name . ' File: ' . $class_file_name, __FILE__, __LINE__, __METHOD__, 10 );
$lf = TTNew( $factory_name );
if ( method_exists( $lf, 'getOptions' ) ) {
if ( method_exists( $lf, 'getAPISearchByCompanyIdAndArrayCriteria' ) ) {
$tmp_columns = $lf->getOptions( 'columns' );
if ( is_array( $tmp_columns ) && count( $tmp_columns ) > 0 ) {
$columns = array_fill_keys( array_keys( array_flip( array_keys( Misc::trimSortPrefix( $lf->getOptions( 'columns' ) ) ) ) ), 'asc' ); //Set sort order to ASC for all columns.
unset( $columns['tag'] ); //Remove columns that we can never sort by.
if ( is_array( $columns ) ) {
try {
if ( !in_array( $factory_name, [ 'BankAccountListFactory' ] ) ) { //Skip legacy factories.
if ( $factory_name == 'MessageControlListFactory' ) {
$retarr = $lf->getAPISearchByCompanyIdAndArrayCriteria( $this->company_id, [ 'current_user_id' => TTUUID::getZeroID() ], 1, 1, null, $columns );
} else if ( $factory_name == 'NotificationListFactory' ) {
$retarr = $lf->getAPISearchByCompanyIdAndArrayCriteria( $this->company_id, [ 'current_user_id' => TTUUID::getZeroID() ], 1, 1, null, $columns );
} else {
//$retarr = $lf->getAPISearchByCompanyIdAndArrayCriteria( $this->company_id, array(), 1, 1, NULL, array('a.bogus' => 'asc') );
$retarr = $lf->getAPISearchByCompanyIdAndArrayCriteria( $this->company_id, [], 1, 1, null, $columns );
}
$this->assertNotEquals( false, $retarr );
$this->assertTrue( is_object( $retarr ), true );
}
} catch ( Exception $e ) {
Debug::Arr( $columns, 'Columns: ', __FILE__, __LINE__, __METHOD__, 10 );
$this->assertTrue( false, $factory_name . ': ' . $e->getMessage() );
}
} else {
Debug::text( 'getOptions(\'columns\') does not return any data, skipping... Factory: ' . $factory_name, __FILE__, __LINE__, __METHOD__, 10 );
}
}
unset( $tmp_columns );
}
} else {
Debug::text( 'getOptions() method does not exist, skipping...', __FILE__, __LINE__, __METHOD__, 10 );
}
Debug::text( 'Done...', __FILE__, __LINE__, __METHOD__, 10 );
return true;
} else {
Debug::text( 'Class does not exist: ' . $factory_name, __FILE__, __LINE__, __METHOD__, 10 );
}
return false;
}
function runSQLTestOnEdition( $product_edition = TT_PRODUCT_ENTERPRISE, $class_list ) {
global $TT_PRODUCT_EDITION, $db;
$original_product_edition = getTTProductEdition();
$this->assertTrue( true );
if ( $product_edition <= $original_product_edition ) {
$TT_PRODUCT_EDITION = $product_edition;
Debug::text( 'Checking against Edition: ' . getTTProductEditionName(), __FILE__, __LINE__, __METHOD__, 10 );
//Loop through all ListFactory classes testing SQL queries.
//Run tests with count rows enabled, then with it disabled as well.
$db->pageExecuteCountRows = false;
foreach ( $class_list as $class_name ) {
$this->runSQLSortTestOnListFactory( $class_name );
$this->runSQLTestOnListFactoryMethods( $class_name );
$this->runSQLTestOnListFactory( $class_name );
}
$db->pageExecuteCountRows = true;
foreach ( $class_list as $class_name ) {
$this->runSQLTestOnListFactoryMethods( $class_name );
$this->runSQLTestOnListFactory( $class_name );
}
}
return true;
}
/**
* @group SQL_CommunityA
*/
function testSQLCommunityA() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_COMMUNITY, $classes[0] );
}
/**
* @group SQL_CommunityB
*/
function testSQLCommunityB() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_COMMUNITY, $classes[1] );
}
/**
* @group SQL_CommunityC
*/
function testSQLCommunityC() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_COMMUNITY, $classes[2] );
}
/**
* @group SQL_CommunityD
*/
function testSQLCommunityD() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_COMMUNITY, $classes[3] );
}
/**
* @group SQL_ProfessionalA
*/
function testSQLProfessionalA() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_PROFESSIONAL, $classes[0] );
}
/**
* @group SQL_ProfessionalB
*/
function testSQLProfessionalB() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_PROFESSIONAL, $classes[1] );
}
/**
* @group SQL_ProfessionalC
*/
function testSQLProfessionalC() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_PROFESSIONAL, $classes[2] );
}
/**
* @group SQL_ProfessionalD
*/
function testSQLProfessionalD() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_PROFESSIONAL, $classes[3] );
}
/**
* @group SQL_CorporateA
*/
function testSQLCorporateA() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_CORPORATE, $classes[0] );
}
/**
* @group SQL_CorporateB
*/
function testSQLCorporateB() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_CORPORATE, $classes[1] );
}
/**
* @group SQL_CorporateC
*/
function testSQLCorporateC() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_CORPORATE, $classes[2] );
}
/**
* @group SQL_CorporateD
*/
function testSQLCorporateD() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_CORPORATE, $classes[3] );
}
/**
* @group SQL_EnterpriseA
*/
function testSQLEnterpriseA() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_ENTERPRISE, $classes[0] );
}
/**
* @group SQL_EnterpriseB
*/
function testSQLEnterpriseB() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_ENTERPRISE, $classes[1] );
}
/**
* @group SQL_EnterpriseC
*/
function testSQLEnterpriseC() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_ENTERPRISE, $classes[2] );
}
/**
* @group SQL_EnterpriseD
*/
function testSQLEnterpriseD() {
$classes = $this->getListFactoryClassList( 4 );
$this->runSQLTestOnEdition( TT_PRODUCT_ENTERPRISE, $classes[3] );
}
/**
* @group SQL_ADODBActiveRecordCount
*/
function testADODBActiveRecordCount() {
global $db;
//This will test the automatic functionality of ADODB to add count(*) in SQL queries.
//PageExecute($query, $limit, $page, $ph)
$db->pageExecuteCountRows = true;
try {
$query = 'SELECT id FROM currency';
$db->PageExecute( $query, 2, 2 );
$this->assertTrue( true );
} catch ( Exception $e ) {
$this->assertTrue( false );
}
try {
$query = 'SELECT id, status_id FROM currency';
$db->PageExecute( $query, 2, 2 );
$this->assertTrue( true );
} catch ( Exception $e ) {
$this->assertTrue( false );
}
try {
$query = 'SELECT a.id FROM currency AS a LEFT JOIN company as tmp ON a.id = tmp.id';
$db->PageExecute( $query, 2, 2 );
$this->assertTrue( true );
} catch ( Exception $e ) {
$this->assertTrue( false );
}
try {
$query = 'SELECT a.id FROM currency AS a LEFT JOIN ( SELECT id FROM company ) as tmp ON a.id = tmp.id';
$db->PageExecute( $query, 2, 2 );
$this->assertTrue( true );
} catch ( Exception $e ) {
$this->assertTrue( false );
}
try {
$query = 'SELECT a.id, a.status_id FROM currency AS a LEFT JOIN ( SELECT id, status_id FROM company ) as tmp ON a.id = tmp.id';
$db->PageExecute( $query, 2, 2 );
$this->assertTrue( true );
} catch ( Exception $e ) {
$this->assertTrue( false );
}
try {
$query = 'SELECT * FROM ( SELECT a.id FROM currency AS a LEFT JOIN company as tmp ON a.id = tmp.id ) as tmp2';
$db->PageExecute( $query, 2, 2 );
$this->assertTrue( true );
} catch ( Exception $e ) {
$this->assertTrue( false );
}
try {
$query = 'SELECT _ADODB_COUNT id, (SELECT 1 FROM currency LIMIT 1) _ADODB_COUNT AS tmp FROM users';
$db->PageExecute( $query, 2, 2 );
$this->assertTrue( true );
} catch ( Exception $e ) {
$this->assertTrue( false );
}
//This query should have the _ADODB_COUNT keyword above to make it work on all databases.
//It works on PGSQL because it can wrap it in a sub-query, ie: SELECT count(*) FROM ( $query )
try {
$query = 'SELECT id, (SELECT 1 FROM currency LIMIT 1) AS tmp FROM users';
$db->PageExecute( $query, 2, 2 );
$this->assertTrue( true ); //PGSQL
} catch ( Exception $e ) {
$this->assertTrue( false ); //PGSQL
}
}
/**
* @group SQL_SQLInjectionA
*/
function testSQLInjectionA() {
//Test SQL injection with SORT SQL.
//Test SQL injection with WHERE SQL.
$utlf = new UserTitleListFactory();
//Test standard case that should work.
$sort_arr = [
'a.name' => 'asc',
];
$utlf->getAPISearchByCompanyIdAndArrayCriteria( 1, [], null, null, null, $sort_arr );
//var_dump( $utlf->rs->sql );
if ( stripos( $utlf->rs->sql, 'a.name' ) !== false ) {
$this->assertTrue( true );
} else {
$this->assertTrue( false );
}
//Test advanced case that should work.
$sort_arr = [
//'a.name = \'test\'' => 'asc'
'a.created_date = 0' => 'asc',
];
$utlf->getAPISearchByCompanyIdAndArrayCriteria( 1, [], null, null, null, $sort_arr );
//var_dump( $utlf->rs->sql );
if ( stripos( $utlf->rs->sql, 'a.created_date = 0' ) !== false ) {
$this->assertTrue( true );
} else {
$this->assertTrue( false );
}
//Test advanced case that does not work currently, but we may need to get to work.
try {
$pself = new PayStubEntryListFactory();
$sort_arr = [
'abs(a.created_date)' => 'asc',
];
$pself->getAPISearchByCompanyIdAndArrayCriteria( 1, [], null, null, null, $sort_arr );
//var_dump( $pself->rs->sql );
if ( stripos( $pself->rs->sql, 'abs(a.created_date)' ) !== false ) {
$this->assertTrue( false );
} else {
$this->assertTrue( true );
}
} catch ( Exception $e ) {
$this->assertTrue( true );
}
//Test SQL injection in the ORDER BY clause.
try {
$sort_arr = [
'created_by' => '(SELECT 1)-- .id.',
];
$utlf->getAPISearchByCompanyIdAndArrayCriteria( 1, [], null, null, null, $sort_arr );
//var_dump( $utlf->rs->sql );
if ( stripos( $utlf->rs->sql, '(SELECT 1)' ) !== false ) {
$this->assertTrue( false );
} else {
$this->assertTrue( true );
}
} catch ( Exception $e ) {
$this->assertTrue( true );
}
//Test SQL injection with brackets and "--"
try {
$sort_arr = [
'(SELECT 1)-- .id.' => 1,
];
$utlf->getAPISearchByCompanyIdAndArrayCriteria( 1, [], null, null, null, $sort_arr );
//var_dump( $utlf->rs->sql );
if ( stripos( $utlf->rs->sql, '(SELECT 1)' ) !== false ) {
$this->assertTrue( false );
} else {
$this->assertTrue( true );
}
} catch ( Exception $e ) {
$this->assertTrue( true );
}
//Test SQL injection with ";" and "--"
try {
$sort_arr = [
'; (SELECT 1)-- .id.' => 1,
];
$utlf->getAPISearchByCompanyIdAndArrayCriteria( 1, [], null, null, null, $sort_arr );
//var_dump( $utlf->rs->sql );
if ( stripos( $utlf->rs->sql, '(SELECT 1)' ) !== false ) {
$this->assertTrue( false );
} else {
$this->assertTrue( true );
}
} catch ( Exception $e ) {
$this->assertTrue( true );
}
//FIXME: Test around the WHERE clause, even though no user input should ever get to it.
// $pslf = TTnew('PayStubListFactory');
// //$pslf->getByCompanyId( 1, 1, NULL, ( array('a.start_date' => ">= '". $pslf->db->BindTimeStamp( TTDate::getBeginDayEpoch( time() - (86400 * 30) ) )."'") ) );
// $pslf->getByCompanyId( 1, 1, NULL, ( array('a.start_date >=' => $pslf->db->BindTimeStamp( TTDate::getBeginDayEpoch( time() - (86400 * 30) ) ) ) ) );
// //$pslf->getByCompanyId( 1, 1, NULL, ( array('a.created_date' => "=1-- (SELECT 1)--") ) );
// var_dump( $pslf->rs->sql );
}
/**
* Test SQL Injection in the sort query.
* @group SQL_SQLInjectionA
*/
function testSortSQLInjectionA() {
$injected_payload = [ "(CASE WHEN (SELECT lo_import('/etc/hosts'))='1' THEN a.created_date ELSE a.updated_date END) ASC --" => 'ASC' ];
//Reflection class required to test as getSortSQL is a protected function.
$uf = new UserFactory();
$class = new ReflectionClass($uf);
$method = $class->getMethod('getSortSQL');
$method->setAccessible(true);
//SQL Payload completely rejected when strict is true.
try {
$method->invokeArgs($uf, [ $injected_payload, true ] );
$this->assertTrue( false );
} catch (Exception $e) {
$this->assertTrue( true );
}
//SQL Payload filtered when strict is false.
$this->assertTrue( $method->invokeArgs( $uf, [ $injected_payload, false ] ) === ' ORDER BY (CASE WHEN (SELECT lo_import(/etc/hosts))=1 THEN a.created_date ELSE a.updated_date END) ASC ASC' );
$this->assertTrue( $method->invokeArgs( $uf, [ [ 'safe"$$;-$-' . "'_characters" => 'ASC' ], false ] ) === ' ORDER BY safe_characters ASC' );
}
/**
* Used to call protected methods in the Factory class.
* @param $name
* @return ReflectionMethod
*/
protected static function getMethod( $name ) {
$class = new ReflectionClass( 'UserListFactory' );
$method = $class->getMethod( $name );
$method->setAccessible( true );
return $method;
}
/**
* @group SQL_testWhereClauseSQL
*/
function testWhereClauseSQL() {
$method = self::getMethod( 'getWhereClauseSQL' );
$ulf = new UserListFactory();
//Boolean TRUE
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', (bool)true, 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 1, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', (int)1, 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 1, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', 1, 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 1, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', 1.00, 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 1, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', '1', 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 1, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', 'TRUE', 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 1, $ph[0] );
//Boolean FALSE
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', (bool)false, 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 0, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', (int)0, 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 0, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', 0, 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 0, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', 0.00, 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 0, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', '0', 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 0, $ph[0] );
$ph = [];
$retval = $method->invokeArgs( $ulf, [ 'a.private', 'FALSE', 'boolean', &$ph ] );
$this->assertEquals( ' AND a.private = ? ', $retval );
$this->assertEquals( 0, $ph[0] );
}
/**
* @group SQL_testTransactionNestingA
*/
function testTransactionNestingA() {
$uf = new UserFactory();
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->StartTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 1, $uf->db->transOff );
$uf->StartTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 2, $uf->db->transOff );
$uf->StartTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 3, $uf->db->transOff );
//$uf->FailTransaction();
$uf->CommitTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 2, $uf->db->transOff );
$uf->CommitTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 1, $uf->db->transOff );
$uf->CommitTransaction();
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
}
/**
* @group SQL_testTransactionNestingB
*/
function testTransactionNestingB() {
$uf = new UserFactory();
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->StartTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 1, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->StartTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 2, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->StartTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 3, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->FailTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 3, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
$uf->CommitTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 2, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
$uf->CommitTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 1, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
$uf->CommitTransaction();
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
}
/**
* @group SQL_testTransactionNestingC
*/
function testTransactionNestingC() {
$uf = new UserFactory();
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->StartTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 1, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->StartTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 2, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->StartTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 3, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->FailTransaction();
$this->assertEquals( 1, $uf->db->transCnt );
$this->assertEquals( 3, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
$uf->CommitTransaction( true ); //Unest all transactions.
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
}
/**
* @group SQL_testTransactionNestingC2
*/
function testTransactionNestingC2() {
$uf = new UserFactory();
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->FailTransaction();
$uf->CommitTransaction( true ); //Unest all transactions.
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
}
/**
* @group SQL_testTransactionNestingC3
*/
function testTransactionNestingC3() {
$uf = new UserFactory();
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( true, $uf->db->_transOK );
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->StartTransaction();
$uf->FailTransaction();
$uf->FailTransaction();
$uf->FailTransaction();
$uf->CommitTransaction( true ); //Unest all transactions.
$this->assertEquals( 0, $uf->db->transCnt );
$this->assertEquals( 0, $uf->db->transOff );
$this->assertEquals( false, $uf->db->_transOK );
}
/**
* @group SQL_testMaximumQueryLengthA
*/
function testMaximumQueryLengthA() {
$ulf = new UserListFactory();
//Build list of many UUIDs.
for( $i = 0; $i < 500000; $i++ ) { //65535 appears to be the max number of parameters for a WHERE IN clause in PostgreSQL.
$ids[] = TTUUID::generateUUID();
}
//This should fail with an exception.
try {
$ulf->getByIdAndCompanyId( $ids, TTUUID::getZeroID() );
//$this->assertTrue( false, $ulf->getRecordCount() ); //This would trigger an asserting exception that would be caught in the catch() below.
$this->assertEquals( 0, $ulf->getRecordCount() ); //Now that we automatically convert UUID arrays to strings without placeholders in getListSQL(), we can exceed the 65535 limit.
} catch ( Exception $e ) {
$this->assertTrue( true, $e->getMessage() );
$this->assertGreaterThan( 10, strlen( $e->getMessage() ) );
$this->assertEquals( -1, $e->getCode() );
}
//This should succeed with still a very high number.
$ids = array_slice( $ids, 0, 5000 );
try {
$ulf->getByIdAndCompanyId( $ids, TTUUID::getZeroID() );
$this->assertTrue( true, $ulf->getRecordCount() );
} catch ( Exception $e ) {
$this->assertTrue( false, $e->getMessage() );
}
}
}