Merge remote-tracking branch 'OpensourcePOS/master' into feat-export-toggle-all

# Conflicts:
#	composer.json
#	composer.lock
#	tests/Controllers/CashupControllerTest.php
#	tests/Controllers/EmployeesControllerTest.php
#	tests/Controllers/ItemKitsControllerTest.php
#	tests/Controllers/SalesControllerTest.php
#	tests/Controllers/TaxesControllerTest.php
#	tests/Helpers/export_helperTest.php
#	tests/Helpers/tabular_helperTest.php
#	tests/Helpers/tax_helperTest.php
This commit is contained in:
objecttothis committed 2026-10-23 11:20:22 +04:00
commit 33efb401bf
76 files changed
+799 -934

No files matched your search

+20
View File
@@ -40,6 +40,16 @@ jobs:
extensions: intl, mbstring, mysqli, gd, bcmath, zip
coverage: none
- name: Verify PHP patch level
run: |
MIN="8.2.35"
ACTUAL=$(php -r 'echo PHP_VERSION;')
if [ "$(printf '%s\n' "$MIN" "$ACTUAL" | sort -V | head -1)" != "$MIN" ]; then
echo "::error::PHP $ACTUAL is below patched minimum $MIN (CVE-2026-109618 / CVE-2026-109619)"
exit 1
fi
echo "PHP $ACTUAL meets minimum $MIN"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -94,6 +104,16 @@ jobs:
env:
GITHUB_TAG: ${{ github.ref_name }}
- name: Normalize version for official release
if: startsWith(github.ref, 'refs/tags/')
run: |
VERSION="${{ steps.version.outputs.version }}"
# The tagged commit may still carry a -dev suffix in source files.
# Strip it so the archive contents report the clean release version.
sed -i "s/application_version = '${VERSION}-dev'/application_version = '${VERSION}'/" app/Config/App.php
sed -i "s/\"version\": \"${VERSION}-dev\"/,\"version\": \"${VERSION}\",/" package.json 2>/dev/null || true
echo "App.php now reports: $(grep application_version app/Config/App.php)"
- name: Validate release tag
if: startsWith(github.ref, 'refs/tags/')
run: |
+16
View File
@@ -43,6 +43,22 @@ jobs:
extensions: tokenizer
coverage: none
- name: Verify PHP patch level
run: |
ACTUAL=$(php -r 'echo PHP_VERSION;')
case "${ACTUAL%.*}" in
8.2) MIN="8.2.35" ;;
8.3) MIN="8.3.36" ;;
8.4) MIN="8.4.27" ;;
8.5) MIN="8.5.12" ;;
*) echo "::error::Unsupported PHP branch: $ACTUAL"; exit 1 ;;
esac
if [ "$(printf '%s\n' "$MIN" "$ACTUAL" | sort -V | head -1)" != "$MIN" ]; then
echo "::error::PHP $ACTUAL is below patched minimum $MIN (CVE-2026-109618 / CVE-2026-109619)"
exit 1
fi
echo "PHP $ACTUAL meets minimum $MIN"
- name: Get composer cache directory
run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
+1 -1
View File
@@ -1,6 +1,6 @@
name: PHP Linting
on: push
on: [push, pull_request]
jobs:
phplint:
+16
View File
@@ -56,6 +56,22 @@ jobs:
extensions: intl, mbstring, mysqli
coverage: none
- name: Verify PHP patch level
run: |
ACTUAL=$(php -r 'echo PHP_VERSION;')
case "${ACTUAL%.*}" in
8.2) MIN="8.2.35" ;;
8.3) MIN="8.3.36" ;;
8.4) MIN="8.4.27" ;;
8.5) MIN="8.5.12" ;;
*) echo "::error::Unsupported PHP branch: $ACTUAL"; exit 1 ;;
esac
if [ "$(printf '%s\n' "$MIN" "$ACTUAL" | sort -V | head -1)" != "$MIN" ]; then
echo "::error::PHP $ACTUAL is below patched minimum $MIN (CVE-2026-109618 / CVE-2026-109619)"
exit 1
fi
echo "PHP $ACTUAL meets minimum $MIN"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
+46
View File
@@ -28,6 +28,52 @@ This document provides guidance for AI agents working on the Open Source Point o
- Tests must pass before submitting changes
- **One test file per class under test.** A controller, model, library, or helper gets exactly one test file covering all of its behavior — `Item_kits.php` → `tests/Controllers/Item_kitsTest.php` (or `Item_kitsControllerTest.php`, matching this codebase's existing `*ControllerTest.php` suffix for controllers), `Sale_lib.php` → `tests/Libraries/Sale_libTest.php`, etc. Do not create feature- or endpoint-scoped test files alongside a class's main test file (e.g. no `Item_kitsBarcodeTest.php` next to `Item_kitsControllerTest.php`) — add the new test methods to the existing file for that class instead. If no test file exists yet for the class, create the one canonical file rather than a narrowly-scoped one.
- **Name test methods as `test{MethodUnderTest}_{Behavior}`.** `PascalCase` the method under test, then an underscore, then a short behavior phrase — `testPostSave_RejectsNegativeTaxPercent`, `testSaveLocale_AcceptsValidReferenceCodeMinMax`. Start the behavior with a verb: `Accepts`/`Requires` for valid or boundary cases, `Rejects` for invalid ones. The name should read as a self-documenting sentence and needs no docblock.
- **Resetting the test database (full migration reset, not just `$refresh`).** When a test class needs the `ospos_tests` schema fully dropped and re-migrated from scratch (e.g. so `ospos_permissions`/`app_config` get freshly seeded), `use Tests\Support\DatabaseBootstrapTrait;` and call `$this->resetDatabaseOnce()` as the first line of `setUp()`, before `parent::setUp()`:
```php
use Tests\Support\DatabaseBootstrapTrait;
class FooTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
protected $migrate = true;
protected $migrateOnce = true;
protected $seedOnce = true;
protected $refresh = false;
protected $namespace = null;
protected function setUp(): void
{
$this->resetDatabaseOnce();
parent::setUp();
// ...
}
}
```
`DatabaseBootstrapTrait` (`tests/Support/DatabaseBootstrapTrait.php`) wraps `TestDatabaseBootstrapSeeder::reset()` with a per-class static guard so it only runs once per test class. Do **not** reinvent this with a local `private static bool $doneBootstrap` flag, `setUpBeforeClass()`, or `Config::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder')` followed by a manual `Config::connect($this->DBGroup)->close()`/`resetDataCache()` in the test file — those are the old patterns the trait replaces, and the manual version reintroduces the exact stale-connection bug `TestDatabaseBootstrapSeeder::reset()` already fixes internally. Only add this trait to test classes that actually touch the real test database (`DatabaseTestTrait` users) — pure unit tests that mock everything don't need it.
If a test class reads `config(OSPOS::class)->settings` for real (not mocking it out), also call `config(OSPOS::class)->update_settings();` right after `parent::setUp();` to force a fresh settings read post-reset. See `tests/Controllers/ReportsControllerTest.php` for that shape.
- **Mocking OSPOS config settings.** When a test needs `config(OSPOS::class)->settings` to return fixed values instead of reading from the database, `use Tests\Support\OsposConfigFixtureTrait;` and call `$this->injectOsposSettings([...])` instead of manually constructing `new OSPOS()` + `Factories::injectMock('config', OSPOS::class, $ospos)`:
```php
use Tests\Support\OsposConfigFixtureTrait;
class FooTest extends CIUnitTestCase
{
use OsposConfigFixtureTrait;
protected function setUp(): void
{
parent::setUp();
$this->injectOsposSettings([
'number_locale' => 'en_US',
'currency_decimals' => 2,
]);
}
}
```
`OSPOS::getDefaultSettings()` is `private` and only covers a handful of keys, so it is not a usable base to merge onto — each test must supply the exact settings it depends on. Use this trait regardless of whether the test also uses `DatabaseBootstrapTrait`.
## Build
+38 -1
View File
@@ -1,4 +1,5 @@
[unreleased]: https://github.com/opensourcepos/opensourcepos/compare/3.4.2...HEAD
[unreleased]: https://github.com/opensourcepos/opensourcepos/compare/3.4.3...HEAD
[3.4.3]: https://github.com/opensourcepos/opensourcepos/compare/3.4.2...3.4.3
[3.4.2]: https://github.com/opensourcepos/opensourcepos/compare/3.4.1...3.4.2
[3.4.1]: https://github.com/opensourcepos/opensourcepos/compare/3.4.0...3.4.1
[3.4.0]: https://github.com/opensourcepos/opensourcepos/compare/3.3.9...3.4.0
@@ -34,6 +35,42 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
## [3.4.3] - 2026-10-21
- chore: bump version to 3.4.3 by @github-actions[bot]
- chore(deps): bump dompurify from 3.4.13 to 3.4.16 (#4723) by @dependabot[bot]
- fix(release): delete the unstable release by name, leave drafts alone by @jekkos
- chore(deps): bump moment from 2.30.1 to 2.31.0 (#4722) by @dependabot[bot]
- chore(deps): bump brace-expansion (#4721) by @dependabot[bot]
- feat(items): optimize search, attribute filtering, and sort for items view (#4652) by @objecttothis
- fix(i18n): add missing toggle_cost_and_profit key to 32 locales (#4712) by @Rayan Abdul Cader
- fix(expenses): refresh table when date range or filters change (#4633) by @richardmilles
- fix(auth): return 401 JSON for AJAX requests on session timeout (#4728) by @jekkos
- docs: add test-method naming convention to AGENTS.md (#4735) by @jekkos
- fix(config): improve responsive layout, nested tabs behavior, and label theming (#4719) by @BudsieBuds
- fix(taxes): reject invalid characters in tax code, category, and jurisdiction names (#4733) by @jekkos
- fix(appconfig): refresh cached settings once and avoid stale same-request reads (#4732) by @jekkos
- Return early from sortAndFilterCart() when no items are printable (#4713) by @Rayan Abdul Cader
- fix: refresh cashups and items tables on filter and date range change (#4731) by @jekkos
- chore: update Node.js version to 22 and npm-check-updates to 23.1.0 in workflows and documentation (#4741) by @BudsieBuds
- fix(models): handle query() returning false in result-consuming methods (#4734) by @jekkos
- feat(reports): add detailed item sales report (#4538) by @enricodelarosa
- fix(taxes): reject negative tax rates and percentages (#4730) by @jekkos
- fix(sales): inline invoice/quote email CSS for DOMPDF PDFs (#4747) (#4749) by @jekkos
- docs: rewrite upgrade guide for the current CI4 install model (#4750) by @jekkos
- refactor: move Windows build scripts into tools/build, drop CI4 migration scripts (#4729) by @jekkos
- feat(config): scope config assets and add system info copy notifications (#4753) by @BudsieBuds
- docs: move BUILD.md, INSTALL.md, UPGRADE.md into docs/ (#4755) by @jekkos
- feat(tables): update loading text size and spinner to bootstrap-table (#4757) by @BudsieBuds
- feat(db): add MySQL 8.4 support (#4048) by @Alexander Laurent
- style(lang): add Georgian, normalize quote style across all locales, add parse tests (#4762) by @objecttothis
- feat(install): add one-line Ubuntu installer (#4550) by @jekkos
- fix(release): bump master version to 3.4.3-dev (#4752) by @jekkos
- fix(config): add invoice_enable and work_order_enable to default settings fallback (#4761) by @Sai Asish Y
- refactor(gulp): modernize build pipeline and streamline dependencies (#4759) by @BudsieBuds
- refactor(tests): normalize test namespaces to PSR-4 and clean up composer dependencies (#4758) by @BudsieBuds
- fix(ci): trigger PHP linting workflow on pull requests (#4764) by @BudsieBuds
- fix(security): pin patched PHP 8.2.35+ for CVE-2026-109618/109619 (#4767) by @jekkos
## [3.4.2] - 2026-09-30
- Fix writable folder permission check (#4270) (#4273) by @jekkos
- Extended payment delete fix (#4274) by @jekkos
+1 -1
View File
@@ -1,4 +1,4 @@
FROM php:8.2-apache AS ospos
FROM php:8.2.35-apache AS ospos
LABEL maintainer="jekkos"
RUN apt-get update && apt-get install -y --no-install-recommends \
+4 -4
View File
@@ -137,7 +137,7 @@ Any person or company found breaching the license agreement might find a bunch o
## 🙏 Credits
| <div align="center">DigitalOcean</div> | <div align="center">JetBrains</div> | <div align="center">GitHub</div> |
| --- | --- | --- |
| <div align="center"><a href="https://www.digitalocean.com?utm_medium=opensource&utm_source=opensourcepos" target="_blank"><img src="https://github.com/user-attachments/assets/fbbf7433-ed35-407d-8946-fd03d236d350" alt="DigitalOcean Logo" height="50"></a></div> | <div align="center"><a href="https://www.jetbrains.com/idea/" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/assets/12870258/187f9bbe-4484-475c-9b58-5e5d5f931f09" alt="IntelliJ IDEA Logo" height="50"></a></div> | <div align="center"><a href="https://github.com/features/actions" target="_blank"><img src="https://github.githubassets.com/images/modules/site/icons/eyebrow-panel/actions-icon.svg" alt="GitHub Actions Logo" height="50"></a></div> |
| Many thanks to [DigitalOcean](https://www.digitalocean.com) for providing the project with hosting credits. | Many thanks to [JetBrains](https://www.jetbrains.com/) for providing a free license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) to kindly support the development of OSPOS. | Many thanks to [GitHub](https://github.com) for providing free continuous integration via GitHub Actions for open-source projects. |
| <div align="center">JetBrains</div> | <div align="center">GitHub</div> |
| --- | --- |
| <div align="center"><a href="https://www.jetbrains.com/idea/" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/assets/12870258/187f9bbe-4484-475c-9b58-5e5d5f931f09" alt="IntelliJ IDEA Logo" height="50"></a></div> | <div align="center"><a href="https://github.com/features/actions" target="_blank"><img src="https://github.githubassets.com/images/modules/site/icons/eyebrow-panel/actions-icon.svg" alt="GitHub Actions Logo" height="50"></a></div> |
| Many thanks to [JetBrains](https://www.jetbrains.com/) for providing a free license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) to kindly support the development of OSPOS. | Many thanks to [GitHub](https://github.com) for providing free continuous integration via GitHub Actions for open-source projects. |
+1 -1
View File
@@ -12,7 +12,7 @@ class App extends BaseConfig
*
* @var string
*/
public string $application_version = '3.4.3-dev';
public string $application_version = '3.4.4-dev';
/**
* This is the commit hash for the version you are currently using.
@@ -25,7 +25,7 @@ class Migration_Initial_Schema extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/initial_schema.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/initial_schema.sql', false, $this->db);
}
/**
@@ -42,7 +42,7 @@ class Migration_Upgrade_To_3_1_1 extends Migration
}
}
if (!executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql')) {
if (!executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql', false, $this->db)) {
throw new DatabaseException('Migration script 3.0.2_to_3.1.1.sql failed. Check logs for details.');
}
@@ -12,7 +12,7 @@ class Migration_Upgrade_To_3_2_0 extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.1.1_to_3.2.0.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.1.1_to_3.2.0.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_Upgrade_To_3_2_1 extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.2.0_to_3.2.1.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.2.0_to_3.2.1.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_Attributes extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_attributes.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_attributes.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_Upgrade_To_3_3_0 extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.2.1_to_3.3.0.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.2.1_to_3.3.0.sql', false, $this->db);
}
/**
@@ -17,7 +17,7 @@ class Migration_IndiaGST extends Migration
// If number of entries is greater than zero then the tax data needs to be migrated
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_indiagst.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_indiagst.sql', false, $this->db);
$count_of_tax_codes = $this->get_count_of_tax_code_entries();
@@ -12,7 +12,7 @@ class Migration_IndiaGST1 extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_indiagst1.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_indiagst1.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_IndiaGST2 extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_indiagst2.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_indiagst2.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_decimal_attribute_type extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_decimal_attribute_type.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_decimal_attribute_type.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_add_iso_4217 extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_add_iso_4217.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_add_iso_4217.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_PaymentTracking extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_paymenttracking.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_paymenttracking.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_RefundTracking extends Migration
public function up(): void
{
helper(['migration', 'locale']);
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_refundtracking.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_refundtracking.sql', false, $this->db);
// Add missing cash_refund amounts to payments table
$decimals = totals_decimals();
@@ -12,7 +12,7 @@ class Migration_DBFix extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_dbfix.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_dbfix.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_fix_attribute_datetime extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_fix_attribute_datetime.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.0_fix_attribute_datetime.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_PaymentDateFix extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.2_paymentdatefix.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.2_paymentdatefix.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_SalesChangePrice extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.2_saleschangeprice.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.2_saleschangeprice.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_modify_attr_links_constraint extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.2_modify_attr_links_constraint.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.2_modify_attr_links_constraint.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_add_item_kit_number extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.3_add_kits_item_number.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.3_add_kits_item_number.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_modify_session_datatype extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.4_modify_session_datatype.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.3.4_modify_session_datatype.sql', false, $this->db);
}
/**
@@ -113,7 +113,7 @@ class Migration_database_optimizations extends Migration
}
$this->db->transComplete();
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.0_database_optimizations.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.0_database_optimizations.sql', false, $this->db);
log_message('info', 'Finished migrating database optimizations.');
}
@@ -27,7 +27,7 @@ class ConvertToCI4 extends Migration
{
helper('migration');
if (!executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.0_CI4Conversion.sql')) {
if (!executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.0_CI4Conversion.sql', false, $this->db)) {
throw new DatabaseException('Migration script 3.4.0_CI4Conversion.sql failed. Check logs for details.');
}
@@ -28,7 +28,7 @@ class fix_duplicate_attributes extends Migration
dropForeignKeyConstraints($foreignKeys, 'attribute_links');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.0_attribute_links_unique_constraint.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.0_attribute_links_unique_constraint.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_sessions_migration extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.1_migrate_sessions_table.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.1_migrate_sessions_table.sql', false, $this->db);
}
/**
@@ -21,7 +21,7 @@ class AttributeLinksUniqueConstraint extends Migration
dropForeignKeyConstraints($foreignKeys, 'attribute_links');
dropColumnIfExists('ospos_attribute_links', 'generated_unique_column');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.1_attribute_links_unique_constraint.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.1_attribute_links_unique_constraint.sql', false, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_MissingConfigKeys extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.2_missing_config_keys.sql', true);
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.2_missing_config_keys.sql', true, $this->db);
}
/**
@@ -12,7 +12,7 @@ class Migration_NullableTaxCategoryId extends Migration
public function up(): void
{
helper('migration');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.2_nullable_tax_category_id.sql');
executeScript(APPPATH . 'Database/Migrations/sqlscripts/3.4.2_nullable_tax_category_id.sql', false, $this->db);
}
/**
@@ -21,7 +21,7 @@ class Migration_Mysql84Utf8mb4Conversion extends Migration
}
$script = APPPATH . 'Database/Migrations/sqlscripts/3.4.3_mysql84_utf8mb4_conversion.sql';
if (!executeScript($script)) {
if (!executeScript($script, false, $this->db)) {
throw new RuntimeException('Failed to execute utf8mb4 conversion migration: ' . $script);
}
@@ -36,7 +36,7 @@ class TestDatabaseBootstrapSeeder extends Seeder
$serverConn->close();
// The application's shared 'tests' connection caches listTables()
// results in dataCache['table_names'']. Because we dropped and
// results in dataCache['table_names']. Because we dropped and
// recreated the schema on a SEPARATE server connection above, that
// cache is now stale. Reset it so subsequent listTables()/tableExists()
// calls re-query the server instead of trusting a dropped schema's table
+73 -64
View File
@@ -8,9 +8,10 @@ use Config\Database;
* Executes a SQL migration script.
* @param string $path Path to migration script.
* @param bool $withTransaction Whether to wrap execution in a transaction.
* @param BaseConnection|null $db Connection to use (defaults to the default connection).
* @return bool Whether the migration executed successfully.
*/
function executeScript(string $path, bool $withTransaction = false): bool
function executeScript(string $path, bool $withTransaction = false, ?BaseConnection $db = null): bool
{
$version = explode('_', basename($path, '.sql'), 2)[0];
log_message('info', "Migrating to $version (file: $path)");
@@ -18,7 +19,7 @@ function executeScript(string $path, bool $withTransaction = false): bool
$sqls = explode(';', file_get_contents($path));
array_pop($sqls);
$db = Database::connect();
$db ??= Database::connect();
if ($withTransaction) {
$db->transStart();
@@ -84,8 +85,8 @@ function dropForeignKeyConstraints(array $foreignKeys, string $table): void
* TODO: This function should be moved to a more global location since it may be needed outside of migrations.
* @return string The prefix before overriding.
*/
function overridePrefix(string $prefix = ''): string {
$db = Database::connect();
function overridePrefix(string $prefix = '', ?BaseConnection $db = null): string {
$db ??= Database::connect();
$originalPrefix = $db->getPrefix();
$db->setPrefix($prefix);
@@ -136,47 +137,50 @@ function createPrimaryKey(string $table, string $index): void {
function dropAllForeignKeyConstraints(?string $table = null, ?string $column = null, ?BaseConnection $db = null): array {
$db ??= Database::connect();
$prefixedTable = $table !== null ? $db->getPrefix() . $table : null;
$prefix = overridePrefix();
$builder = $db->table('information_schema.KEY_COLUMN_USAGE kcu');
$builder->distinct();
$builder->select('kcu.CONSTRAINT_NAME, kcu.TABLE_NAME, kcu.COLUMN_NAME, kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME, kcu.ORDINAL_POSITION, rc.DELETE_RULE, rc.UPDATE_RULE');
$builder->join(
'information_schema.REFERENTIAL_CONSTRAINTS rc',
'kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA AND kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND kcu.TABLE_NAME = rc.TABLE_NAME',
'left'
);
if ($table !== null && $column !== null) {
$scopedBuilder = $db->table('information_schema.KEY_COLUMN_USAGE scoped');
$scopedBuilder->distinct();
$scopedBuilder->select('scoped.CONSTRAINT_NAME, scoped.TABLE_NAME');
$scopedBuilder->where('scoped.TABLE_SCHEMA', $db->database);
$scopedBuilder->groupStart();
$scopedBuilder->where('scoped.REFERENCED_TABLE_NAME', $prefixedTable);
$scopedBuilder->where('scoped.REFERENCED_COLUMN_NAME', $column);
$scopedBuilder->groupEnd();
$scopedBuilder->orGroupStart();
$scopedBuilder->where('scoped.TABLE_NAME', $prefixedTable);
$scopedBuilder->where('scoped.COLUMN_NAME', $column);
$scopedBuilder->groupEnd();
$prefix = overridePrefix('', $db);
try {
$builder = $db->table('information_schema.KEY_COLUMN_USAGE kcu');
$builder->distinct();
$builder->select('kcu.CONSTRAINT_NAME, kcu.TABLE_NAME, kcu.COLUMN_NAME, kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME, kcu.ORDINAL_POSITION, rc.DELETE_RULE, rc.UPDATE_RULE');
$builder->join(
'(' . $scopedBuilder->getCompiledSelect() . ') matched',
'matched.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND matched.TABLE_NAME = kcu.TABLE_NAME',
'inner',
false
'information_schema.REFERENTIAL_CONSTRAINTS rc',
'kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA AND kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND kcu.TABLE_NAME = rc.TABLE_NAME',
'left'
);
if ($table !== null && $column !== null) {
$scopedBuilder = $db->table('information_schema.KEY_COLUMN_USAGE scoped');
$scopedBuilder->distinct();
$scopedBuilder->select('scoped.CONSTRAINT_NAME, scoped.TABLE_NAME');
$scopedBuilder->where('scoped.TABLE_SCHEMA', $db->database);
$scopedBuilder->groupStart();
$scopedBuilder->where('scoped.REFERENCED_TABLE_NAME', $prefixedTable);
$scopedBuilder->where('scoped.REFERENCED_COLUMN_NAME', $column);
$scopedBuilder->groupEnd();
$scopedBuilder->orGroupStart();
$scopedBuilder->where('scoped.TABLE_NAME', $prefixedTable);
$scopedBuilder->where('scoped.COLUMN_NAME', $column);
$scopedBuilder->groupEnd();
$builder->join(
'(' . $scopedBuilder->getCompiledSelect() . ') matched',
'matched.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND matched.TABLE_NAME = kcu.TABLE_NAME',
'inner',
false
);
}
$builder->where('kcu.TABLE_SCHEMA', $db->database);
$builder->where('rc.CONSTRAINT_NAME IS NOT NULL', null, false);
$builder->orderBy('kcu.CONSTRAINT_NAME');
$builder->orderBy('kcu.ORDINAL_POSITION');
$result = $builder->get();
} finally {
overridePrefix($prefix, $db);
}
$builder->where('kcu.TABLE_SCHEMA', $db->database);
$builder->where('rc.CONSTRAINT_NAME IS NOT NULL', null, false);
$builder->orderBy('kcu.CONSTRAINT_NAME');
$builder->orderBy('kcu.ORDINAL_POSITION');
$result = $builder->get();
overridePrefix($prefix);
if ($result === false) {
$detail = 'unknown error';
try {
@@ -346,18 +350,20 @@ function foreignKeyExists(string $constraintName, string $tableName): bool {
$prefix = overridePrefix();
$db = Database::connect();
$builder = $db->table('INFORMATION_SCHEMA.TABLE_CONSTRAINTS');
$builder->select('CONSTRAINT_NAME');
$builder->where('TABLE_SCHEMA', $db->database);
$builder->where('TABLE_NAME', $prefix . $tableName);
$builder->where('CONSTRAINT_TYPE', 'FOREIGN KEY');
$builder->where('CONSTRAINT_NAME', $constraintName);
$query = $builder->get();
try {
$db = Database::connect();
$builder = $db->table('INFORMATION_SCHEMA.TABLE_CONSTRAINTS');
$builder->select('CONSTRAINT_NAME');
$builder->where('TABLE_SCHEMA', $db->database);
$builder->where('TABLE_NAME', $prefix . $tableName);
$builder->where('CONSTRAINT_TYPE', 'FOREIGN KEY');
$builder->where('CONSTRAINT_NAME', $constraintName);
$query = $builder->get();
overridePrefix($prefix);
return $query->getNumRows() > 0;
return $query->getNumRows() > 0;
} finally {
overridePrefix($prefix);
}
}
/**
@@ -371,23 +377,26 @@ function dropColumnIfExists(string $table, string $column): void
{
$prefix = overridePrefix();
$db = Database::connect();
$builder = $db->table('information_schema.COLUMNS');
try {
$db = Database::connect();
$builder = $db->table('information_schema.COLUMNS');
// Check if the column exists in the table
$builder->select('COLUMN_NAME')
->where('TABLE_SCHEMA', $db->database)
->where('TABLE_NAME', $prefix . $table)
->where('COLUMN_NAME', $column);
// Check if the column exists in the table
$builder->select('COLUMN_NAME')
->where('TABLE_SCHEMA', $db->database)
->where('TABLE_NAME', $prefix . $table)
->where('COLUMN_NAME', $column);
$query = $builder->get();
$query = $builder->get();
if ($query->getNumRows() > 0)
{
// Drop the column if it exists
$db->query("ALTER TABLE `" . $prefix . "$table` DROP COLUMN `$column`");
if ($query->getNumRows() > 0)
{
// Drop the column if it exists
$db->query("ALTER TABLE `" . $prefix . "$table` DROP COLUMN `$column`");
}
} finally {
overridePrefix($prefix);
}
overridePrefix($prefix);
}
/**
+7 -11
View File
@@ -25,27 +25,22 @@
"matrix": "https://matrix.to/#/#opensourcepos_Lobby:gitter.im"
},
"require": {
"php": "^8.2",
"php": ">=8.2.35 <8.3 || >=8.3.36 <8.4 || >=8.4.27",
"ext-intl": "*",
"codeigniter4/framework": "4.7.4",
"dompdf/dompdf": "^3.1.6",
"ezyang/htmlpurifier": "^4.17",
"laminas/laminas-escaper": "2.18.0",
"ezyang/htmlpurifier": "^4.19",
"openspout/openspout": "^4.28",
"paragonie/random_compat": "^2.0.21",
"picqer/php-barcode-generator": "^2.4.0",
"setasign/tfpdf": "^1.34",
"tamtamchik/namecase": "^3.0.0"
},
"require-dev": {
"codeigniter/coding-standard": "^1.8",
"fakerphp/faker": "^1.23.0",
"friendsofphp/php-cs-fixer": "^3.47.1",
"codeigniter/coding-standard": "^1.9",
"friendsofphp/php-cs-fixer": "^3.95",
"kint-php/kint": "^5.0.4",
"mikey179/vfsstream": "^1.6",
"nexusphp/cs-config": "^3.6",
"phpunit/phpunit": "^10.5.16 || ^11.2",
"predis/predis": "^1.1 || ^2.0"
"nexusphp/cs-config": "^3.29",
"phpunit/phpunit": "^10.5.16 || ^11.2"
},
"replace": {
"psr/log": "*"
@@ -53,6 +48,7 @@
"autoload": {
"psr-4": {
"App\\": "app/",
"Config\\": "app/Config/",
"CodeIgniter\\": "vendor/codeigniter4/framework/system/"
},
"exclude-from-classmap": [
Generated
+312 -630
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,6 +1,6 @@
## Server Requirements
- PHP version `8.2` to `8.4` are supported, PHP version `≤ 8.1` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section.
- PHP version `8.2.35` to `8.4.27` are supported (minimum patch levels required for CVE-2026-109618 / CVE-2026-109619 security fixes). PHP version `≤ 8.1` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section.
- MySQL `5.7` is supported, also MariaDB replacement `10.x` is supported and might offer better performance.
- Apache `2.4` is supported. Nginx should work fine too, see [wiki page here](https://github.com/opensourcepos/opensourcepos/wiki/Local-Deployment-using-LEMP).
- Raspberry PI based installations proved to work, see [wiki page here](<https://github.com/opensourcepos/opensourcepos/wiki/Installing-on-Raspberry-PI---Orange-PI-(Headless-OSPOS)>).
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@opensourcepos/opensourcepos",
"version": "3.4.3",
"version": "3.4.4-dev",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@opensourcepos/opensourcepos",
"version": "3.4.3",
"version": "3.4.4-dev",
"license": "MIT",
"dependencies": {
"bootstrap": "^3.4.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opensourcepos/opensourcepos",
"version": "3.4.3",
"version": "3.4.4-dev",
"description": "Open Source Point of Sale is a web based point of sale system written in the PHP language. It uses MySQL as the data storage back-end and has a simple user interface.",
"keywords": [
"point-of-sale",
+4 -4
View File
@@ -7,9 +7,9 @@ use App\Models\Employee;
use CodeIgniter\Config\Factories;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\Test\CIUnitTestCase;
use Config\OSPOS;
use Config\Services;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\Support\OsposConfigFixtureTrait;
/**
* Regression tests for the login validation flow in OSPOSRules::login_check().
@@ -19,6 +19,8 @@ use PHPUnit\Framework\MockObject\MockObject;
*/
class OSPOSRulesTest extends CIUnitTestCase
{
use OsposConfigFixtureTrait;
protected function tearDown(): void
{
Factories::reset();
@@ -138,9 +140,7 @@ class OSPOSRulesTest extends CIUnitTestCase
private function injectSettings(array $settings): void
{
$ospos = new OSPOS();
$ospos->settings = $settings;
Factories::injectMock('config', OSPOS::class, $ospos);
$this->injectOsposSettings($settings);
}
private function injectRequest(): void
+7 -16
View File
@@ -3,22 +3,23 @@
namespace Tests\Controllers;
use App\Models\Cashup;
use CodeIgniter\Config\Factories;
use CodeIgniter\Database\Config;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
use Config\OSPOS;
use ReflectionProperty;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\EmployeeFixtureTrait;
use Tests\Support\OsposConfigFixtureTrait;
class CashupControllerTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use FeatureTestTrait;
use EmployeeFixtureTrait;
use OsposConfigFixtureTrait;
protected $migrate = true;
protected $migrateOnce = true;
@@ -26,34 +27,24 @@ class CashupControllerTest extends CIUnitTestCase
protected $refresh = false;
protected $namespace = null;
private static bool $doneBootstrap = false;
protected function setUp(): void
{
if (self::$doneBootstrap === false) {
Config::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder');
Config::connect($this->DBGroup)->close();
self::$doneBootstrap = true;
}
$this->resetDatabaseOnce();
parent::setUp();
$ospos = new OSPOS();
$ospos->settings = [
$this->injectOsposSettings([
'company' => 'Test Co',
'dateformat' => 'Y-m-d',
'timeformat' => 'H:i:s',
'number_locale' => 'en_US',
'currency_decimals' => 2,
'thousands_separator' => ',',
];
Factories::injectMock('config', OSPOS::class, $ospos);
]);
}
protected function tearDown(): void
{
Factories::reset();
parent::tearDown();
}
+3 -9
View File
@@ -2,16 +2,17 @@
namespace Tests\Controllers;
use CodeIgniter\Database\Config;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
use App\Models\Customer;
use App\Models\Employee;
use Tests\Support\DatabaseBootstrapTrait;
class CustomersCsvImportTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use FeatureTestTrait;
protected $migrate = true;
@@ -22,19 +23,12 @@ class CustomersCsvImportTest extends CIUnitTestCase
protected Customer $customer;
protected Employee $employee;
private static bool $doneBootstrap = false;
protected function setUp(): void
{
// Reset the test database to a clean schema on the first test in this
// class so leftover customers with the same emails from prior runs
// cannot be rejected as duplicates by the import (check_email_exists).
if (self::$doneBootstrap === false) {
Config::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder');
Config::connect($this->DBGroup)->close();
self::$doneBootstrap = true;
}
$this->resetDatabaseOnce();
parent::setUp();
// Reset any stale transaction state left on the shared in-process DB
@@ -2,7 +2,6 @@
namespace Tests\Controllers;
use CodeIgniter\Database\Config;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
@@ -12,11 +11,13 @@ use CodeIgniter\Config\Services;
use App\Models\Employee;
use App\Models\Module;
use ReflectionProperty;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\EmployeeFixtureTrait;
class EmployeesControllerTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use FeatureTestTrait;
use EmployeeFixtureTrait;
@@ -27,20 +28,13 @@ class EmployeesControllerTest extends CIUnitTestCase
protected $priorDisallowGrantChange;
private static bool $doneBootstrap = false;
protected function setUp(): void
{
// Reset the test database to a known clean schema on the first test in
// this class so stale employees from prior runs (whose usernames are
// unique-key bound and whose grant sets leak into these assertions)
// cannot contaminate the assertions below.
if (self::$doneBootstrap === false) {
Config::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder');
Config::connect($this->DBGroup)->close();
self::$doneBootstrap = true;
}
$this->resetDatabaseOnce();
parent::setUp();
// Reset any stale transaction state left on the shared in-process DB
+3 -9
View File
@@ -2,12 +2,12 @@
namespace Tests\Controllers;
use CodeIgniter\Database\Config;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
use App\Models\Employee;
use RuntimeException;
use Tests\Support\DatabaseBootstrapTrait;
/**
* Test suite for Home controller password validation
@@ -19,6 +19,7 @@ use RuntimeException;
class HomeTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use FeatureTestTrait;
protected $migrate = true;
@@ -26,16 +27,9 @@ class HomeTest extends CIUnitTestCase
protected $refresh = false;
protected $namespace = null;
private static bool $doneBootstrap = false;
protected function setUp(): void
{
if (self::$doneBootstrap === false) {
Config::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder');
Config::connect($this->DBGroup)->close();
self::$doneBootstrap = true;
}
$this->resetDatabaseOnce();
parent::setUp();
}
+7 -16
View File
@@ -2,8 +2,6 @@
namespace Tests\Controllers;
use CodeIgniter\Config\Factories;
use CodeIgniter\Database\Config;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
@@ -11,14 +9,17 @@ use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
use App\Models\Item;
use App\Models\Item_kit;
use Config\OSPOS;
use Exception;
use ReflectionProperty;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\OsposConfigFixtureTrait;
class ItemKitsControllerTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use FeatureTestTrait;
use OsposConfigFixtureTrait;
protected $migrate = true;
protected $migrateOnce = true;
@@ -26,24 +27,16 @@ class ItemKitsControllerTest extends CIUnitTestCase
protected $refresh = false;
protected $namespace = null;
private static bool $doneBootstrap = false;
protected Item $item;
protected Item_kit $itemKit;
protected function setUp(): void
{
if (self::$doneBootstrap === false) {
Config::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder');
Config::connect($this->DBGroup)->close();
self::$doneBootstrap = true;
}
$this->resetDatabaseOnce();
parent::setUp();
$ospos = new OSPOS();
$ospos->settings = [
$this->injectOsposSettings([
'company' => 'Test Co',
'barcode_content' => 'id',
'barcode_type' => 'C128',
@@ -59,8 +52,7 @@ class ItemKitsControllerTest extends CIUnitTestCase
'barcode_page_cellspacing' => 1,
'barcode_generate_if_empty' => 0,
'barcode_formats' => 'null',
];
Factories::injectMock('config', OSPOS::class, $ospos);
]);
$this->item = model(Item::class);
$this->itemKit = model(Item_kit::class);
@@ -68,7 +60,6 @@ class ItemKitsControllerTest extends CIUnitTestCase
protected function tearDown(): void
{
Factories::reset();
parent::tearDown();
}
+3 -8
View File
@@ -6,9 +6,9 @@ use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
use App\Database\Seeds\TestDatabaseBootstrapSeeder;
use App\Models\Employee;
use Config\OSPOS;
use Tests\Support\DatabaseBootstrapTrait;
/**
* Regression tests for the reports permission bypass
@@ -24,6 +24,7 @@ use Config\OSPOS;
class ReportsControllerTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use FeatureTestTrait;
protected $migrate = true;
@@ -31,18 +32,12 @@ class ReportsControllerTest extends CIUnitTestCase
protected $refresh = false;
protected $namespace = null;
private static bool $doneBootstrap = false;
/**
* Set up test environment
*/
protected function setUp(): void
{
if (self::$doneBootstrap === false) {
TestDatabaseBootstrapSeeder::reset();
self::$doneBootstrap = true;
}
$this->resetDatabaseOnce();
parent::setUp();
+3 -9
View File
@@ -2,7 +2,6 @@
namespace Tests\Controllers;
use CodeIgniter\Database\Config;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
@@ -12,6 +11,7 @@ use CodeIgniter\Config\Services;
use App\Models\Employee;
use Config\Database;
use ReflectionProperty;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\ItemFixtureTrait;
use Tests\Support\EmployeeFixtureTrait;
use Tests\Support\SaleFixtureTrait;
@@ -31,6 +31,7 @@ use Tests\Support\SaleFixtureTrait;
class SalesControllerTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use FeatureTestTrait;
use ItemFixtureTrait;
use EmployeeFixtureTrait;
@@ -42,16 +43,9 @@ class SalesControllerTest extends CIUnitTestCase
protected $refresh = false;
protected $namespace = null;
private static bool $doneBootstrap = false;
protected function setUp(): void
{
if (self::$doneBootstrap === false) {
Config::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder');
Config::connect($this->DBGroup)->close();
self::$doneBootstrap = true;
}
$this->resetDatabaseOnce();
parent::setUp();
}
+3 -9
View File
@@ -2,7 +2,6 @@
namespace Tests\Controllers;
use CodeIgniter\Database\Config;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Test\CIUnitTestCase;
@@ -10,11 +9,13 @@ use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
use App\Models\Tax;
use ReflectionProperty;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\EmployeeFixtureTrait;
class TaxesControllerTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use FeatureTestTrait;
use EmployeeFixtureTrait;
@@ -24,16 +25,9 @@ class TaxesControllerTest extends CIUnitTestCase
protected $refresh = false;
protected $namespace = null;
private static $doneBootstrap = false;
protected function setUp(): void
{
if (self::$doneBootstrap === false) {
Config::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder');
Config::connect($this->DBGroup)->close();
self::$doneBootstrap = true;
}
$this->resetDatabaseOnce();
parent::setUp();
}
@@ -2,13 +2,14 @@
namespace Tests\Helpers;
use CodeIgniter\Config\Factories;
use CodeIgniter\Test\CIUnitTestCase;
use Config\OSPOS;
use stdClass;
use Tests\Support\OsposConfigFixtureTrait;
class GetItemDataRowTest extends CIUnitTestCase
{
use OsposConfigFixtureTrait;
protected function setUp(): void
{
parent::setUp();
@@ -20,8 +21,7 @@ class GetItemDataRowTest extends CIUnitTestCase
protected function injectSettings(array $overrides = []): void
{
$config = new OSPOS();
$config->settings = array_merge([
$this->injectOsposSettings(array_merge([
'multi_pack_enabled' => 0,
'use_destination_based_tax' => 0,
'number_locale' => 'en_US',
@@ -29,9 +29,7 @@ class GetItemDataRowTest extends CIUnitTestCase
'quantity_decimals' => 0,
'thousands_separator' => 1,
'currency_symbol' => '$',
], $overrides);
Factories::injectMock('config', OSPOS::class, $config);
], $overrides));
}
protected function makeItem(array $overrides = []): stdClass
@@ -1,19 +1,20 @@
<?php
use CodeIgniter\Config\Factories;
namespace Tests\Helpers;
use CodeIgniter\Test\CIUnitTestCase;
use Config\OSPOS;
use Tests\Support\OsposConfigFixtureTrait;
class LocaleHelperTest extends CIUnitTestCase
{
use OsposConfigFixtureTrait;
protected function setUp(): void
{
parent::setUp();
require_once __DIR__ . '/../../app/Helpers/locale_helper.php';
$config = new OSPOS();
$config->settings = ['dateformat' => 'Y-m-d'];
Factories::injectMock('config', OSPOS::class, $config);
$this->injectOsposSettings(['dateformat' => 'Y-m-d']);
}
public function testValidDateReturnsTrue(): void
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Tests\Helpers;
use PHPUnit\Framework\TestCase;
class UrlHelperTest extends TestCase
{
protected function setUp(): void
{
// Include the url_helper.php file
require_once __DIR__ . '/../../app/Helpers/url_helper.php';
}
public function testBase64urlEncode(): void
{
$data = 'Test data';
$encoded = base64url_encode($data);
// Assert that the encoded string is URL-safe
$this->assertMatchesRegularExpression('/^[A-Za-z0-9\-_]+$/', $encoded);
// Assert that decoding the encoded string returns the original data
$decoded = base64url_decode($encoded);
$this->assertEquals($data, $decoded);
}
}
File renamed without changes.
@@ -1,6 +1,6 @@
<?php
namespace Tests\helpers;
namespace Tests\Helpers;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Test\CIUnitTestCase;
@@ -1,10 +1,13 @@
<?php
namespace Tests\Helpers;
use App\Libraries\CI3SecretConverter;
use App\Models\Appconfig;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Encryption as EncryptionConfig;
use Config\Services;
use RuntimeException;
/**
* The filesystem-touching helpers read their target paths from the shared
File renamed without changes.
File renamed without changes.
+5 -5
View File
@@ -14,10 +14,12 @@ use App\Models\Sale;
use App\Models\Stock_location;
use CodeIgniter\Config\Factories;
use CodeIgniter\Test\CIUnitTestCase;
use Config\OSPOS;
use Tests\Support\OsposConfigFixtureTrait;
class Sale_libPaymentTest extends CIUnitTestCase
{
use OsposConfigFixtureTrait;
private Sale_lib $saleLib;
protected int $priorBcscale;
@@ -35,15 +37,13 @@ class Sale_libPaymentTest extends CIUnitTestCase
bcscale(0);
// Inject mock OSPOS config so Sale_lib constructor and helpers don't need real settings
$ospos = new OSPOS();
$ospos->settings = [
$this->injectOsposSettings([
'cash_rounding_code' => '',
'cash_decimals' => 2,
'currency_decimals' => 2,
'tax_decimals' => 2,
'quantity_decimals' => 2,
];
Factories::injectMock('config', OSPOS::class, $ospos);
]);
// Inject stub models so model() calls in Sale_lib::__construct() skip DB
$stubMethods = ['__construct'];
+6 -4
View File
@@ -5,12 +5,13 @@ namespace Tests\Models;
use App\Models\Appconfig;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Config\Database;
use Config\OSPOS;
use Tests\Support\DatabaseBootstrapTrait;
class AppconfigTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
protected $migrate = true;
protected $migrateOnce = true;
@@ -19,10 +20,11 @@ class AppconfigTest extends CIUnitTestCase
protected $refresh = true;
protected $namespace = null;
public static function setUpBeforeClass(): void
protected function setUp(): void
{
$seeder = Database::seeder('tests');
$seeder->call('TestDatabaseBootstrapSeeder');
$this->resetDatabaseOnce();
parent::setUp();
}
private function appconfig(): Appconfig
+4 -7
View File
@@ -5,8 +5,8 @@ namespace Tests\Models;
use App\Models\Customer;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Config\Database;
use Tests\Support\ConcurrentDbRaceTrait;
use Tests\Support\DatabaseBootstrapTrait;
/**
* Regression tests: adjustRewardPoints() must apply
@@ -17,6 +17,7 @@ use Tests\Support\ConcurrentDbRaceTrait;
class CustomerRewardPointsTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use ConcurrentDbRaceTrait;
protected $migrate = true;
@@ -24,14 +25,10 @@ class CustomerRewardPointsTest extends CIUnitTestCase
protected $refresh = false;
protected $namespace = null;
public static function setUpBeforeClass(): void
{
$seeder = Database::seeder('tests');
$seeder->call('TestDatabaseBootstrapSeeder');
}
protected function setUp(): void
{
$this->resetDatabaseOnce();
parent::setUp();
}
+5
View File
@@ -5,20 +5,25 @@ namespace Tests\Models;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use App\Models\Employee;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\EmployeeFixtureTrait;
class EmployeeTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use EmployeeFixtureTrait;
protected $migrate = true;
protected $migrateOnce = true;
protected $seedOnce = true;
protected $refresh = false;
protected $namespace = null;
protected function setUp(): void
{
$this->resetDatabaseOnce();
parent::setUp();
}
+4 -7
View File
@@ -5,8 +5,8 @@ namespace Tests\Models;
use App\Models\Giftcard;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Config\Database;
use Tests\Support\ConcurrentDbRaceTrait;
use Tests\Support\DatabaseBootstrapTrait;
/**
* Regression tests: decrementGiftcardValue() must
@@ -17,6 +17,7 @@ use Tests\Support\ConcurrentDbRaceTrait;
class GiftcardTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use ConcurrentDbRaceTrait;
protected $migrate = true;
@@ -24,14 +25,10 @@ class GiftcardTest extends CIUnitTestCase
protected $refresh = false;
protected $namespace = null;
public static function setUpBeforeClass(): void
{
$seeder = Database::seeder('tests');
$seeder->call('TestDatabaseBootstrapSeeder');
}
protected function setUp(): void
{
$this->resetDatabaseOnce();
parent::setUp();
}
+9 -8
View File
@@ -5,6 +5,7 @@ namespace Tests\Models;
use App\Models\Item;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Tests\Support\DatabaseBootstrapTrait;
/**
* Regression coverage for mass assignment in bulk edit.
@@ -15,15 +16,13 @@ use CodeIgniter\Test\DatabaseTestTrait;
class ItemBulkUpdateTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
/**
* The schema is taken as it stands rather than migrated, so these tests run
* against an existing test database without dropping it. Every row created
* here is rolled back in tearDown().
*/
protected $migrate = false;
protected $refresh = false;
protected $namespace = null;
protected $migrate = true;
protected $migrateOnce = true;
protected $seedOnce = true;
protected $refresh = false;
protected $namespace = null;
protected Item $item;
@@ -35,6 +34,8 @@ class ItemBulkUpdateTest extends CIUnitTestCase
protected function setUp(): void
{
$this->resetDatabaseOnce();
parent::setUp();
$this->item = model(Item::class);
+4 -7
View File
@@ -5,8 +5,8 @@ namespace Tests\Models;
use App\Models\Item_quantity;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Config\Database;
use Tests\Support\ConcurrentDbRaceTrait;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\ItemFixtureTrait;
/**
@@ -20,6 +20,7 @@ use Tests\Support\ItemFixtureTrait;
class ItemQuantityTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use ConcurrentDbRaceTrait;
use ItemFixtureTrait;
@@ -30,14 +31,10 @@ class ItemQuantityTest extends CIUnitTestCase
private const LOCATION_ID = 1;
public static function setUpBeforeClass(): void
{
$seeder = Database::seeder('tests');
$seeder->call('TestDatabaseBootstrapSeeder');
}
protected function setUp(): void
{
$this->resetDatabaseOnce();
parent::setUp();
}
+4 -7
View File
@@ -5,13 +5,14 @@ namespace Tests\Models;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use App\Models\Item;
use Config\Database;
use Config\OSPOS;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\ItemSearchFixtureTrait;
class ItemSearchTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use ItemSearchFixtureTrait;
protected $migrate = true;
@@ -23,14 +24,10 @@ class ItemSearchTest extends CIUnitTestCase
protected $item;
public static function setUpBeforeClass(): void
{
$seeder = Database::seeder('tests');
$seeder->call('TestDatabaseBootstrapSeeder');
}
protected function setUp(): void
{
$this->resetDatabaseOnce();
parent::setUp();
$this->item = model(Item::class);
+4 -7
View File
@@ -5,12 +5,13 @@ namespace Tests\Models;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use App\Models\Item_taxes;
use Config\Database;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\ItemSearchFixtureTrait;
class ItemTaxesMultipleTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use ItemSearchFixtureTrait;
protected $migrate = true;
@@ -22,14 +23,10 @@ class ItemTaxesMultipleTest extends CIUnitTestCase
protected $item_taxes;
public static function setUpBeforeClass(): void
{
$seeder = Database::seeder('tests');
$seeder->call('TestDatabaseBootstrapSeeder');
}
protected function setUp(): void
{
$this->resetDatabaseOnce();
parent::setUp();
$this->item_taxes = model(Item_taxes::class);
@@ -7,13 +7,16 @@ namespace Tests\Models\Reports;
use App\Models\Reports\Summary_taxes;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Tests\Support\DatabaseBootstrapTrait;
class Summary_taxes_test extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
protected $migrate = true;
protected $migrateOnce = true;
protected $seedOnce = true;
protected $refresh = false;
protected $namespace = null;
@@ -22,6 +25,8 @@ class Summary_taxes_test extends CIUnitTestCase
protected function setUp(): void
{
$this->resetDatabaseOnce();
parent::setUp();
$this->seedTestData();
}
+3 -9
View File
@@ -3,9 +3,9 @@
namespace Tests\Models;
use App\Models\Sale;
use CodeIgniter\Database\Config;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Tests\Support\DatabaseBootstrapTrait;
use Tests\Support\EmployeeFixtureTrait;
use Tests\Support\FailedQueryConnectionTrait;
use Tests\Support\ItemFixtureTrait;
@@ -18,6 +18,7 @@ use Tests\Support\ItemFixtureTrait;
class SaleTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use DatabaseBootstrapTrait;
use EmployeeFixtureTrait;
use FailedQueryConnectionTrait;
use ItemFixtureTrait;
@@ -28,18 +29,11 @@ class SaleTest extends CIUnitTestCase
protected $refresh = false;
protected $namespace = null;
private static bool $doneBootstrap = false;
private const LOCATION_ID = 1;
protected function setUp(): void
{
if (self::$doneBootstrap === false) {
Config::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder');
Config::connect($this->DBGroup)->close();
self::$doneBootstrap = true;
}
$this->resetDatabaseOnce();
parent::setUp();
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace Tests\Support;
use App\Database\Seeds\TestDatabaseBootstrapSeeder;
use CodeIgniter\Database\Config;
/**
* Standard one-time-per-class database reset for DatabaseTestTrait tests.
*
* Call {@see self::resetDatabaseOnce()} as the first line of setUp(), before
* parent::setUp(). It drops and re-migrates the `tests` schema exactly once
* per test class (guarded by a per-class static, since PHP trait statics are
* independent per using-class) and closes the shared connection afterward so
* the migration run that follows reconnects against the freshly reset schema
* instead of reusing a connection whose state predates the reset.
*
* Usage:
*
* use Tests\Support\DatabaseBootstrapTrait;
*
* class FooTest extends CIUnitTestCase
* {
* use DatabaseTestTrait;
* use DatabaseBootstrapTrait;
*
* protected function setUp(): void
* {
* $this->resetDatabaseOnce();
*
* parent::setUp();
* }
* }
*/
trait DatabaseBootstrapTrait
{
private static bool $doneBootstrap = false;
protected function resetDatabaseOnce(): void
{
if (self::$doneBootstrap === true) {
return;
}
TestDatabaseBootstrapSeeder::reset();
Config::connect($this->DBGroup)->close();
self::$doneBootstrap = true;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace Tests\Support;
use CodeIgniter\Config\Factories;
use Config\OSPOS;
/**
* Shared helper for injecting a mocked OSPOS config into a test.
*
* Replaces the old per-file `$ospos = new OSPOS(); $ospos->settings = [...];
* Factories::injectMock(...)` block that was duplicated with slightly
* different settings arrays across many test files. Each test supplies the
* exact settings it depends on - OSPOS::getDefaultSettings() is private and
* only has a handful of keys, so it is not a usable base to merge onto.
*
* Usage:
*
* use Tests\Support\OsposConfigFixtureTrait;
*
* class FooTest extends CIUnitTestCase
* {
* use OsposConfigFixtureTrait;
*
* protected function setUp(): void
* {
* parent::setUp();
*
* $this->injectOsposSettings([
* 'invoice_enable' => false,
* 'number_locale' => 'en_US',
* ]);
* }
* }
*/
trait OsposConfigFixtureTrait
{
/**
* @param array<string, mixed> $settings
*/
protected function injectOsposSettings(array $settings): OSPOS
{
$ospos = new OSPOS();
$ospos->settings = $settings;
Factories::injectMock('config', OSPOS::class, $ospos);
return $ospos;
}
}
-25
View File
@@ -1,25 +0,0 @@
<?php
use PHPUnit\Framework\TestCase;
class UrlHelperTest extends TestCase
{
protected function setUp(): void
{
// Include the url_helper.php file
require_once __DIR__ . '/../../app/Helpers/url_helper.php';
}
public function testBase64urlEncode(): void
{
$data = 'Test data';
$encoded = base64url_encode($data);
// Assert that the encoded string is URL-safe
$this->assertMatchesRegularExpression('/^[A-Za-z0-9\-_]+$/', $encoded);
// Assert that decoding the encoded string returns the original data
$decoded = base64url_decode($encoded);
$this->assertEquals($data, $decoded);
}
}