626 lines
20 KiB
PHP
626 lines
20 KiB
PHP
<?php
|
|
|
|
namespace ElixirAudit\Tests;
|
|
|
|
use BRSystems\Core\Library\cCoreSystem\cCoreSystem;
|
|
use BRSystems\Core\Library\cDb\cDb;
|
|
use BRSystems\Core\Library\cDefuseCrypto\cDefuseCrypto;
|
|
use BRSystems\Core\Library\cGoogle2FA\cGoogle2FA;
|
|
use BRSystems\Core\Library\cINI\cINI;
|
|
use BRSystems\Core\Library\cUrl\cUrl;
|
|
use Exception;
|
|
use Http\Client\Curl\Client;
|
|
use MyCLabs\Enum\Enum;
|
|
use Nyholm\Psr7\Factory\Psr17Factory;
|
|
use PHPUnit\Framework\TestCase;
|
|
use BRSystems\Core\App\pubUsers\pubUsers;
|
|
use BRSystems\Core\App\pubUsers\inc\pubUsersController;
|
|
use ReflectionClass;
|
|
use rpkamp\Mailhog\MailhogClient;
|
|
use rpkamp\Mailhog\Message\Contact;
|
|
use rpkamp\Mailhog\Message\Message;
|
|
use rpkamp\Mailhog\NoSuchMessageException;
|
|
use BRSystems\Core\Sys\sysUsers\sysUsers;
|
|
use ElixirAudit\Tests\DataSetup\Constants\siteMap;
|
|
use ElixirAudit\Tests\DataSetup\Constants\siteUsers;
|
|
|
|
/**
|
|
* @method static DbTitleEnum E_ELIXIR()
|
|
*/
|
|
final class DbTitleEnum extends Enum
|
|
{
|
|
private const E_ELIXIR = 'elixir';
|
|
}
|
|
|
|
abstract class BasePhpUnitCase extends TestCase
|
|
{
|
|
/**
|
|
* @var cDb
|
|
*/
|
|
protected cDb $cDb;
|
|
|
|
/**
|
|
* @var String - Mysql DB title
|
|
*/
|
|
protected string $mysqlName = 'maison_alexandra_lv';
|
|
|
|
/**
|
|
* @var String - Mysql connection user
|
|
*/
|
|
protected string $mysqlUser = 'dAccGlobal';
|
|
|
|
/**
|
|
* @var String - Mysql connection user password
|
|
*/
|
|
protected string $mysqlPass = 'dAccGlobal12!@';
|
|
|
|
/**
|
|
* @var String - Mysql connection host
|
|
*/
|
|
protected string $mysqlHost = 'maisonalexandra_mysql';
|
|
|
|
/**
|
|
* @var String - Mysql connection port
|
|
*/
|
|
protected string $mysqlPort = '3306';
|
|
|
|
/**
|
|
* @var Int - Mail send delay timeout
|
|
*/
|
|
protected int $mailSendTimeout = 14;
|
|
|
|
/**
|
|
* @var Int - Server port
|
|
*/
|
|
protected int $serverPort = 80;
|
|
|
|
/**
|
|
* @var bool - Unit test case
|
|
*/
|
|
protected bool $unitTestsAreRunning = false;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected string $mailHogAPIRequestUrl = 'http://maisonalexandra_mailhog:8025/';
|
|
|
|
protected pubUsers $pubUser;
|
|
protected sysUsers $cmsUser;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
/*** define the site path constant ***/
|
|
if(!defined('__MDIR__')) {
|
|
define('__MDIR__', realpath(__DIR__ . '/../www/') . '/');
|
|
}
|
|
|
|
// Set default
|
|
ini_set('date.timezone', 'Europe/Riga');
|
|
date_default_timezone_set('Europe/Riga');
|
|
|
|
ini_set('display_errors', 'On');
|
|
ini_set('error_reporting', E_ALL);
|
|
|
|
// Increase time & memory limit
|
|
set_time_limit(60 * 60 * 3);
|
|
ini_set('memory_limit', '2048M');
|
|
|
|
// Define base sql constants for system to use
|
|
if (!defined('DB_HOST')) define('DB_HOST', $this->mysqlHost);
|
|
if (!defined('DB_USR')) define('DB_USR', $this->mysqlUser);
|
|
if (!defined('DB_PWD')) define('DB_PWD', $this->mysqlPass);
|
|
if (!defined('DB_PORT')) define('DB_PORT', $this->mysqlPort);
|
|
if (!defined('DB_NAME')) define('DB_NAME', $this->mysqlName);
|
|
|
|
// To be replaced by namespaces later!!!!
|
|
include_once(__DIR__ . "/../www/system-brs-core/config/constants.php");
|
|
include_once(__DIR__ . "/../www/system-brs-core/functions/com.funct.php");
|
|
|
|
// Set handler functions
|
|
// Setup without namespaces before namespaces ar implemented into project - system autoloader
|
|
set_error_handler('errorHandler');
|
|
|
|
if ($this->unitTestsAreRunning) {
|
|
|
|
// Set flag while global tests are running
|
|
$GLOBALS['coverage_tests_running'] = true;
|
|
|
|
// Load site configuration file
|
|
$oCfg = new cINI(__CFG__ . 'init.cfg.ini');
|
|
$oCfg->loadSystemConstants();
|
|
|
|
// Set default values host, server port, etc.
|
|
if (
|
|
empty($_SERVER['HTTP_HOST'])
|
|
&& $sDefaultHost = $oCfg->getV('system', 'DEFAULT_HOST')
|
|
) {
|
|
$_SERVER['HTTP_HOST'] = $sDefaultHost;
|
|
}
|
|
if (empty($_SERVER['SERVER_PORT'])) {
|
|
$_SERVER['SERVER_PORT'] = $this->serverPort;
|
|
}
|
|
foreach (['SERVER_ADDR', 'REQUEST_URI', 'REMOTE_ADDR'] as $key) {
|
|
if (empty($_SERVER[$key])) {
|
|
$_SERVER[$key] = $this->serverPort;
|
|
}
|
|
}
|
|
|
|
// Set request type - while testing POST needs to be specified
|
|
$this->setUnitTestRequestType();
|
|
}
|
|
|
|
$this->cDb = cDb::getInstance($this->mysqlHost, $this->mysqlUser, $this->mysqlPass, $this->mysqlPort, $this->mysqlName, 'SeleniumDbConnection');
|
|
$this->cDb->disableDispose = true;
|
|
|
|
// Reload base data tables
|
|
$this->reloadDbTables(['mod_users', 'mod_account_manager']);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
parent::tearDown();
|
|
}
|
|
|
|
protected function dbSetup(array $files): void
|
|
{
|
|
try {
|
|
foreach ($files as $file) {
|
|
$file = __DIR__ . '/DataSetup/' . $file;
|
|
if (!file_exists($file)) {
|
|
throw new Exception('DB setup file not found: ' . $file);
|
|
}
|
|
|
|
$sql = preg_split('/[.+;][\s]*\n/', file_get_contents($file), -1, PREG_SPLIT_NO_EMPTY);
|
|
if (!is_array($sql)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($sql as $query) {
|
|
if (empty(trim($query))) {
|
|
continue;
|
|
}
|
|
$this->cDb->query($query);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->fail($e->getMessage());
|
|
}
|
|
}
|
|
|
|
protected function reloadDbTables(array $tables = null, DbTitleEnum $baseTable = null): void
|
|
{
|
|
if (is_null($baseTable)) {
|
|
$baseTable = DbTitleEnum::E_ZUTENIS();
|
|
}
|
|
if (!is_null($tables)) {
|
|
foreach ($tables as $table) {
|
|
$this->cDb->query("TRUNCATE TABLE `{$baseTable}`.`{$table}`");
|
|
$this->cDb->query("LOAD DATA INFILE '/opt/data/{$baseTable}/$table.txt' INTO TABLE `{$baseTable}`.`{$table}`");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function to exec insert with a data array
|
|
*
|
|
* @param string $table - table name
|
|
* @param array $data - fields to insert with key to field structure
|
|
* @return array|bool - array with ids
|
|
*/
|
|
protected function insertTestDataMulti(string $table, array $data)
|
|
{
|
|
if (empty($data)) {
|
|
return false;
|
|
}
|
|
|
|
$ids = $this->cDb->_queryInsertArray($table, $data);
|
|
return $ids ?: false;
|
|
}
|
|
|
|
protected function updateTestDataMulti(string $table, array $data, string $idColumn = 'id'): bool
|
|
{
|
|
if (empty($data) || !$idColumn) {
|
|
return false;
|
|
}
|
|
|
|
return $this->cDb->_queryUpdateMulti($table, $data, $idColumn);
|
|
}
|
|
|
|
protected function assertExpected(array $expectedData, string $output, bool $convertToLowercase = false): void
|
|
{
|
|
foreach ($expectedData as $expected) {
|
|
if ($convertToLowercase) {
|
|
$this->assertStringContainsString(mb_strtolower($expected), mb_strtolower($output));
|
|
} else {
|
|
$this->assertStringContainsString($expected, $output);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function emailHtmlDecode(string $data = ''): string
|
|
{
|
|
return html_entity_decode($data, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
|
|
}
|
|
|
|
protected function getAndSetStructureForFirstMatchedUrl(int $mapId): array
|
|
{
|
|
$url = new cUrl();
|
|
$urlData = getFirstMatchByMapId($mapId);
|
|
|
|
$_SERVER['SERVER_PORT'] = 80;
|
|
$_SERVER['HTTPS'] = 'off';
|
|
$_SERVER['HTTP_HOST'] = 'local.elixir';
|
|
$_SERVER['REQUEST_URI'] = $url->rmSlashesIndex($urlData['p_full_url']);
|
|
$_SERVER['REMOTE_ADDR'] = '';
|
|
$_SERVER['SERVER_ADDR'] = '';
|
|
|
|
return $urlData;
|
|
}
|
|
|
|
protected function mprint(): void
|
|
{
|
|
// Get all arguments
|
|
$vPassedArguments = func_get_args();
|
|
if(!!$vPassedArguments){
|
|
$sDebugOutput = PHP_EOL . 'START DEBUG OUTPUT:' . PHP_EOL;
|
|
foreach($vPassedArguments as $vData) $sDebugOutput .= PHP_EOL . print_r($vData, true) . PHP_EOL;
|
|
die($sDebugOutput . PHP_EOL . 'END OF DEBUG!');
|
|
}
|
|
die('NO ARGUMENTS PASSED!');
|
|
}
|
|
|
|
function getLoginCredentialsByType(string $userType = siteUsers::loginCmsAdmin): array
|
|
{
|
|
return siteUsers::loginData[$userType] ?? [];
|
|
}
|
|
|
|
protected function setUnitTestRequestType(bool $isGET = true, bool $isPOST = false): void
|
|
{
|
|
if ($isPOST) {
|
|
$_SERVER['REQUEST_METHOD'] = 'POST';
|
|
}
|
|
if (!defined('POST_REQ')) define('POST_REQ', $isPOST);
|
|
if (!defined('GET_REQ')) define('GET_REQ', $isGET);
|
|
}
|
|
|
|
protected function initBaseClassObjectBySiteMapId(
|
|
string $siteMapId = SiteMap::PUB_LOGIN,
|
|
?string $loginUser = siteUsers::loginAppSelfEmployed,
|
|
bool $security2FAEnabled = true,
|
|
int $selYear = 2022,
|
|
array $unlockYear = [2026, 2025, 2024, 2023, 2022]
|
|
): ?object {
|
|
$app = new cCoreSystem();
|
|
$this->getAndSetStructureForFirstMatchedUrl($siteMapId);
|
|
|
|
// Check if from previous test user is not still logged in - otherwise log out user
|
|
$this->logOutUnitTestUser();
|
|
|
|
// System modules requires users to be logged in
|
|
if (!is_null($loginUser)) {
|
|
$this->loginUnitTestUserForCoverage($loginUser, false, $security2FAEnabled);
|
|
}
|
|
|
|
// Return null on 2FA disabled
|
|
if ($security2FAEnabled) {
|
|
return $app->launch(true);
|
|
} else {
|
|
try {
|
|
$app->launch(true);
|
|
} catch (Exception $error) {
|
|
$this->assertExpected(
|
|
['Redirected to Location: /ienakt-gramatvedibas-sistema/divu-faktoru-iestatisana'],
|
|
$error->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function loginUnitTestUserForCoverage(
|
|
string $loginUser = siteUsers::loginAppSelfEmployed,
|
|
bool $isCmsUser = false,
|
|
bool $security2FAEnabled = true
|
|
): void {
|
|
// Simulate user login
|
|
if ($isCmsUser) {
|
|
$this->cmsUser = new sysUsers(getFirstMatchByMapId(SiteMap::PUB_LOGIN));
|
|
} else {
|
|
$this->pubUser = new pubUsers(getFirstMatchByMapId(SiteMap::PUB_LOGIN));
|
|
|
|
// If user already logged in continue
|
|
if ($this->pubUser->controller->isLoggedIn()) {
|
|
return;
|
|
}
|
|
|
|
$userData = $this->getLoginCredentialsByType($loginUser);
|
|
|
|
// Log in & catch redirect exception
|
|
try {
|
|
$this->pubUser->controller->loginUser(
|
|
[
|
|
'chkTerms' => 1,
|
|
'user_login' => $userData['user'],
|
|
'user_password' => $userData['password'],
|
|
]
|
|
);
|
|
} catch (Exception $error) {
|
|
if ($security2FAEnabled) {
|
|
$this->assertExpected(['Redirected to Location'], $error->getMessage());
|
|
} else {
|
|
$this->assertExpected(
|
|
['"jsonok":true,', '"url":"\/gramatvedibas-sistema\/konta-izvele"'],
|
|
$error->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
// Submit 2fa security & catch json send data exception
|
|
if ($security2FAEnabled) {
|
|
try {
|
|
$oGoogle2FA = new cGoogle2FA();
|
|
$next2FACode = $oGoogle2FA->getGoogleAuthCode($userData['2fa']);
|
|
|
|
$_POST['code'] = $next2FACode;
|
|
$this->pubUser->controller->set2FAForm();
|
|
} catch (Exception $error) {
|
|
$this->assertExpected(['"jsonok":true'], $error->getMessage());
|
|
}
|
|
}
|
|
|
|
// Check if user company is set
|
|
if ($this->pubUser->controller->selCompId) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function logOutUnitTestUser(bool $isCmsUser = false): void
|
|
{
|
|
try {
|
|
if ($isCmsUser) {
|
|
$this->cmsUser->controller->logOut();
|
|
} else {
|
|
if (!isset($this->pubUser)) {
|
|
$this->pubUser = new pubUsers(getFirstMatchByMapId(SiteMap::PUB_LOGIN));
|
|
}
|
|
$this->pubUser->controller->logOut();
|
|
}
|
|
} catch (Exception $error) {
|
|
if ($isCmsUser) {
|
|
$this->assertFalse($this->cmsUser->controller->isLoggedIn());
|
|
} else {
|
|
$this->assertFalse($this->pubUser->controller->isLoggedIn());
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function resetGetAndPost(array $data = [], bool $destroyPrev = true): void
|
|
{
|
|
$this->setPost();
|
|
$this->setGet();
|
|
}
|
|
|
|
protected function destroyUserSession(): void
|
|
{
|
|
unset($_SESSION[pubUsersController::USER_SESSION_KEY]);
|
|
unset($_SESSION[pubUsersController::USER_2FA_SESSION_KEY]);
|
|
unset($_SESSION[pubUsersController::USER_PWD_SESSION_KEY]);
|
|
}
|
|
|
|
protected function setPost(array $data = [], bool $destroyPrev = true): void
|
|
{
|
|
if ($destroyPrev) {
|
|
$_POST = [];
|
|
}
|
|
|
|
if (!!$data) {
|
|
foreach ($data as $key => $value) {
|
|
$_POST[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function setGet(array $data = [], bool $destroyPrev = true): void
|
|
{
|
|
if ($destroyPrev) {
|
|
$_GET = [];
|
|
}
|
|
|
|
if (!!$data) {
|
|
foreach ($data as $key => $value) {
|
|
$_GET[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function testModuleLoginRequirement(string $siteMapId = SiteMap::PUB_LOGIN): void
|
|
{
|
|
try {
|
|
$this->logOutUnitTestUser();
|
|
// Create new class
|
|
$this->initBaseClassObjectBySiteMapId($siteMapId, null);
|
|
} catch (Exception $error) {
|
|
$this->assertExpected(
|
|
['Redirected to Location: /ienakt-gramatvedibas-sistema'],
|
|
$error->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
protected function clearMailHogMessages(): void
|
|
{
|
|
$client = new MailhogClient(new Client(), new Psr17Factory(), new Psr17Factory(), $this->mailHogAPIRequestUrl);
|
|
$client->purgeMessages();
|
|
}
|
|
|
|
protected function awaitEmailFromMailHog(
|
|
string $recipient = null,
|
|
string $subject = null,
|
|
int $timeout = 30,
|
|
bool $presumeMailboxIsEmpty = false
|
|
): Message {
|
|
$reqTime = time() - 5;
|
|
// Create connection to MailHog
|
|
$client = new MailhogClient(new Client(), new Psr17Factory(), new Psr17Factory(), $this->mailHogAPIRequestUrl);
|
|
|
|
$time = 0;
|
|
$error = null;
|
|
|
|
while ($time <= $timeout) {
|
|
|
|
// Init timeout
|
|
sleep(1);
|
|
$time++;
|
|
|
|
try {
|
|
$messages = $client->getLastMessage();
|
|
$messageTime = strtotime($messages->headers->get('date'));
|
|
|
|
// Ignore older messages and empty mailbox flag is not set
|
|
if (!$presumeMailboxIsEmpty && $messageTime < $reqTime) {
|
|
continue;
|
|
}
|
|
|
|
# Search for recipient & title
|
|
if (!is_null($recipient) && !is_null($subject)) {
|
|
if (
|
|
$messages->recipients->contains(Contact::fromString($recipient)) &&
|
|
$messages->subject == $subject
|
|
) {
|
|
return $messages;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
# Search for recipient
|
|
if (
|
|
!is_null($recipient) &&
|
|
$messages->recipients->contains(Contact::fromString($recipient))
|
|
) {
|
|
return $messages;
|
|
}
|
|
|
|
# Search for title
|
|
if (
|
|
!is_null($subject) &&
|
|
$messages->subject == $subject
|
|
) {
|
|
return $messages;
|
|
}
|
|
|
|
// Default
|
|
if (
|
|
is_null($recipient) &&
|
|
is_null($subject)
|
|
) {
|
|
return $messages;
|
|
}
|
|
} catch (NoSuchMessageException $error) {
|
|
// Continue waiting for message
|
|
}
|
|
}
|
|
|
|
// Max timeout reached
|
|
throw new NoSuchMessageException($error);
|
|
}
|
|
|
|
public function setProtectedProperty($classObject, $property, $value): void
|
|
{
|
|
$reflection = new ReflectionClass($classObject);
|
|
$reflection_property = $reflection->getProperty($property);
|
|
$reflection_property->setAccessible(true);
|
|
$reflection_property->setValue($classObject, $value);
|
|
}
|
|
|
|
public function setUserPermission(string $permissionKey): void
|
|
{
|
|
// Add pub user permission without ID
|
|
$_SESSION['pub-user']['permissions'][] = $permissionKey;
|
|
}
|
|
public function lockOrUnlockCompanyDetailsYear(int $year, bool $locked): void
|
|
{
|
|
// After usage needs to reload table mod_account_manager && clear apc from db
|
|
$partners = $this->initBaseClassObjectBySiteMapId(SiteMap::PUB_PARTNERS);
|
|
$partners->controller->saveYearsSettings(['id' => $year, 'locked' => $locked ? 1 : 0 ]);
|
|
}
|
|
|
|
public function getSqlDataInjectionCfg(string $tableName = 'some_db_table'): array
|
|
{
|
|
return [
|
|
'SMPOL' => "DROP $tableName;--",
|
|
'M' => "DROP $tableName;#",
|
|
'M2' => "data' --",
|
|
|
|
'Bypassing login screens (SMO+)' => "admin' --",
|
|
'Bypassing login screens (SMO+) 1' => "admin' #",
|
|
'Bypassing login screens (SMO+) 2' => "admin'/*",
|
|
'Bypassing login screens (SMO+) 3' => "' or 1=1--",
|
|
'Bypassing login screens (SMO+) 4' => "' or 1=1#",
|
|
'Bypassing login screens (SMO+) 5' => "' or 1=1/*",
|
|
'Bypassing login screens (SMO+) 6' => "') or '1'='1--",
|
|
'Bypassing login screens (SMO+) 7' => "') or ('1'='1--",
|
|
|
|
'Obfuscation' => "test;DROP/*comment*/$tableName",
|
|
'Breaking up a keyword to avoid filters' => ";DR/**/OP/*bypass blacklisting*/$tableName",
|
|
'Removing space characters' => ";SELECT/*avoid-spaces*/password/**/FROM/**/$tableName",
|
|
'ID value' => "10; DROP TABLE $tableName /*",
|
|
'ID value 2' => "10; DROP TABLE $tableName --",
|
|
'Stacking Queries - MSP' => "1; DROP $tableName--",
|
|
'Stacking Queries - MSP ID value' => "10;DROP $tableName --",
|
|
|
|
|
|
'Error-based ways to discover column information' => "' HAVING 1=1",
|
|
'Simple INSERT payload' => "'; insert into $tableName values( 1, 'hax0r', 'coolpass', 9 )/*",
|
|
|
|
'sample insert data' => "''ert'' // */=-+12!@#$%^&*()",
|
|
'sample insert data 2' => "āēšģīķļžčņ",
|
|
'html' => "<b>test data </b>",
|
|
];
|
|
}
|
|
|
|
public function updateCfgStructure(
|
|
string $updateText,
|
|
array $structure,
|
|
array $fields = [],
|
|
array $excludeFields = []
|
|
): array
|
|
{
|
|
$updatedFields = $structure;
|
|
if (empty($structure) || !strlen($updateText)) {
|
|
return $structure;
|
|
}
|
|
|
|
// Keep only the requested fields
|
|
if (!empty($fields)) {
|
|
$updatedFields = $fields;
|
|
}
|
|
|
|
// Get all fields except the passed excluded
|
|
if (!empty($excludeFields)) {
|
|
$updatedFields = array_diff_key($updatedFields, array_flip($excludeFields));
|
|
}
|
|
|
|
// Fill data from config
|
|
$filledArray = array_fill_keys(array_keys($updatedFields), $updateText);
|
|
|
|
return array_merge($structure , $filledArray);
|
|
}
|
|
|
|
protected function switchDbSchema(DbTitleEnum $schema)
|
|
{
|
|
$this->cDb = cDb::getInstance(
|
|
$this->mysqlHost,
|
|
$this->mysqlUser,
|
|
$this->mysqlPass,
|
|
$this->mysqlPort,
|
|
$schema,
|
|
'SeleniumDbConnection',
|
|
);
|
|
$this->cDb->disableDispose = true;
|
|
}
|
|
}
|