From c6bbf61b3d1edb6e329148be9d176b23536864ee Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 10:52:27 +0000 Subject: [PATCH 1/2] Build the package here, with one version number for everything The package (what users install) had a repository of its own, codeling/pkg_bfstop, into which the plugin and component zips were copied and committed by hand - nothing made sure they were built from the code of the release, or even of the same version (its main had 1.5.2's zips next to a 2.0.0 manifest). - package/: the package manifest, its installer script and update.xml, moved from pkg_bfstop, and build.sh, which zips plugin and component with their own deploy.sh (from copies of the checkouts) and builds the package from them into build/, with SHA-256 sums. Nothing is committed. - tests/lint/check-versions.php: the version is written in bfstop.xml of plugin and component, in pkg_bfstop.xml and update.xml and in the CHANGELOG; they have to agree (build.sh refuses otherwise), and with --release= they have to be the tag and the CHANGELOG entry needs its date. The CHANGELOG says "2.0.0 (unreleased)" until then. - deploy.sh takes the version from bfstop.xml and starts with a new zip instead of adding to an old one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AFJFMMUpch4LGexEiXN8Nc --- .gitignore | 1 + CHANGELOG | 2 +- README | 3 + deploy.sh | 3 +- package/README.md | 35 +++++++++++ package/build.sh | 53 +++++++++++++++++ package/pkg_bfstop.xml | 22 +++++++ package/script.php | 23 +++++++ package/update.xml | 4 ++ tests/lint/check-versions.php | 109 ++++++++++++++++++++++++++++++++++ 10 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 package/README.md create mode 100755 package/build.sh create mode 100644 package/pkg_bfstop.xml create mode 100644 package/script.php create mode 100644 package/update.xml create mode 100644 tests/lint/check-versions.php diff --git a/.gitignore b/.gitignore index 400b527..ca207f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor/ *.zip +/build/ diff --git a/CHANGELOG b/CHANGELOG index 946f78d..bc306d7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -2.0.0 (2026-08-24) +2.0.0 (unreleased) - Migrate to Joomla 5/6: PSR-4 namespaced classes (Codeling\Plugin\System\Bfstop), service provider, SubscriberInterface event handling - Drop Joomla 3/4 compatibility (requires Joomla 5 or 6 and PHP 8.1); the diff --git a/README b/README index 2074d36..4d8e54a 100644 --- a/README +++ b/README @@ -9,6 +9,9 @@ https://github.com/codeling/bfstop/wiki For information on what changed in recent versions, see the CHANGELOG file +The package users install (plugin and component together) is built from this +repository and com_bfstop by package/build.sh, see package/README.md. + If you find any issues, please report them at https://github.com/codeling/bfstop/issues diff --git a/deploy.sh b/deploy.sh index 2fec4ff..0286ee4 100755 --- a/deploy.sh +++ b/deploy.sh @@ -15,10 +15,11 @@ langfiles="language" docs="CHANGELOG LICENSE.txt README" plgtype="system" langs="de-DE en-GB" -version=2.0.0 +version="$(sed -n 's:.*\(.*\).*:\1:p' "$(dirname "$0")/$extname.xml" | head -n 1)" if [ "$1" == "zip" ] then + rm -f $extname-$version.zip zip -r $extname-$version.zip $srcfiles $docs $langfiles exit fi diff --git a/package/README.md b/package/README.md new file mode 100644 index 0000000..7395732 --- /dev/null +++ b/package/README.md @@ -0,0 +1,35 @@ +# The BFStop package + +What users install: one Joomla! package (`pkg_bfstop-.zip`) with the +plugin (this repository) and the component +([codeling/com_bfstop](https://github.com/codeling/com_bfstop)) in it. It used +to have a repository of its own, codeling/pkg_bfstop, with the extension zips +committed into it by hand. + +- `pkg_bfstop.xml`: the package manifest. +- `script.php`: the package's installer script; refuses Joomla! < 5.0 and + PHP < 8.1 before anything is installed (sites on Joomla! 4 stay with 1.5). +- `update.xml`: the update server's collection file + (`https://bfstop.bfroehler.info/update.xml`, which installed sites ask). +- `build.sh`: builds the zips. + +## Building + + package/build.sh [] + +zips the plugin and the component with their own `deploy.sh`, from copies of +the checkouts, and puts them and the package into `build/` (or the given +directory), with a `SHA256SUMS-` file. Nothing is committed: CI builds +the package for every push and pull request and keeps it as an artifact of the +run ("bfstop-"), installs it in all its Joomla! sites and updates a +site of the previous release with it (see `tests/README.md`). + +## Versions + +Plugin, component and package are released together, with one version number. +It is written in `bfstop.xml` of both repositories, in `pkg_bfstop.xml` and +`update.xml`, and in the first entry of `CHANGELOG`, which says `(unreleased)` +until the release, then the date. `tests/lint/check-versions.php` checks that +they agree (CI runs it, and `build.sh` refuses to build otherwise); +`--release=` additionally checks them against the tag and requires the +CHANGELOG date. The `deploy.sh` scripts take the version from their manifest. diff --git a/package/build.sh b/package/build.sh new file mode 100755 index 0000000..a5fb636 --- /dev/null +++ b/package/build.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# +# Builds the release zips of BFStop: the plugin (this repository), the +# component (a checkout of codeling/com_bfstop) and the package containing +# both, which is what users install. The checkouts are not changed; the zips +# go to the output directory (default: build/ in this repository): +# +# bfstop-.zip, com_bfstop-.zip, pkg_bfstop-.zip +# +# All three must have the same version (see tests/lint/check-versions.php), +# so a package can't contain the extensions of another release. +# +# Usage: package/build.sh [] +set -euo pipefail + +plugin_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +component_root="$(cd "${1:?usage: package/build.sh []}" && pwd)" +out="${2:-$plugin_root/build}" +mkdir -p "$out" +out="$(cd "$out" && pwd)" + +COM_BFSTOP_ROOT="$component_root" php "$plugin_root/tests/lint/check-versions.php" +version="$(php "$plugin_root/tests/lint/check-versions.php" --print-version)" + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +# each extension is zipped by its own deploy.sh, in a copy of its checkout +# (deploy.sh writes the zip next to its sources) +build_extension() { + local src="$1" zip="$2" + mkdir -p "$work/src" + rm -rf "$work/src/ext" + cp -a "$src" "$work/src/ext" + rm -f "$work/src/ext"/*.zip + (cd "$work/src/ext" && ./deploy.sh zip > /dev/null) + mv "$work/src/ext/$zip" "$out/$zip" + php "$plugin_root/tests/lint/check-zip.php" "$out/$zip" bfstop.xml > /dev/null +} +build_extension "$plugin_root" "bfstop-$version.zip" +build_extension "$component_root" "com_bfstop-$version.zip" + +mkdir -p "$work/pkg/packages" +cp "$plugin_root/package/pkg_bfstop.xml" "$plugin_root/package/script.php" "$work/pkg/" +cp "$out/bfstop-$version.zip" "$work/pkg/packages/plg_sys_bfstop.zip" +cp "$out/com_bfstop-$version.zip" "$work/pkg/packages/com_bfstop.zip" +rm -f "$out/pkg_bfstop-$version.zip" +(cd "$work/pkg" && zip -qr "$out/pkg_bfstop-$version.zip" pkg_bfstop.xml script.php packages) +php "$plugin_root/tests/lint/check-zip.php" "$out/pkg_bfstop-$version.zip" pkg_bfstop.xml > /dev/null + +(cd "$out" && sha256sum "bfstop-$version.zip" "com_bfstop-$version.zip" "pkg_bfstop-$version.zip" > "SHA256SUMS-$version") +echo "Built in $out:" +cat "$out/SHA256SUMS-$version" diff --git a/package/pkg_bfstop.xml b/package/pkg_bfstop.xml new file mode 100644 index 0000000..ed23855 --- /dev/null +++ b/package/pkg_bfstop.xml @@ -0,0 +1,22 @@ + + + Brute Force Stop Package + Bernhard Froehler + May 2013 + Copyright (C) by Bernhard Froehler + GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html + bfstop + 2.0.0 + https://github.com/codeling/bfstop + Bernhard Froehler + https://github.com/codeling/bfstop + Brute Force Stop Package successfully installed!<br><br><div>This package contains all extensions constituting the Brute Force Stop (bfstop) functionality. It provides means to avert Brute-Force-Attacks on your Joomla-Installation.</div><br><div>For a list of all currently blocked IP addresses (and the chance to unblock them), go to "Components" - "Brute Force Stop Administration"<br><br>If you have any questions or problems, please visit the <a href='https://github.com/codeling/bfstop'>project site on github</a>.<br>In case you want to uninstall Brute Force Stop, please use the Package to uninstall all extensions at once!<br>Brute Force Stop is published under the <a target='_blank' title='GNU General Public License' href='http://www.gnu.org/copyleft/gpl.html'>GNU General Public License</a>.</div> + script.php + + com_bfstop.zip + plg_sys_bfstop.zip + + + https://bfstop.bfroehler.info/update.xml + + diff --git a/package/script.php b/package/script.php new file mode 100644 index 0000000..e9f228d --- /dev/null +++ b/package/script.php @@ -0,0 +1,23 @@ + + + + diff --git a/tests/lint/check-versions.php b/tests/lint/check-versions.php new file mode 100644 index 0000000..d1119f2 --- /dev/null +++ b/tests/lint/check-versions.php @@ -0,0 +1,109 @@ + ... are , and the + * CHANGELOG entry has a date + * php tests/lint/check-versions.php --print-version prints the plugin's version +**/ + +$root = dirname(__DIR__, 2); +$release = null; +foreach (array_slice($argv, 1) as $arg) +{ + if ($arg === '--print-version') + { + echo manifestVersion($root.'/bfstop.xml')."\n"; + exit(0); + } + if (str_starts_with($arg, '--release=')) + { + $release = substr($arg, strlen('--release=')); + continue; + } + fwrite(STDERR, "Usage: php {$argv[0]} [--release= | --print-version]\n"); + exit(2); +} + +function manifestVersion($file) +{ + $xml = @simplexml_load_file($file); + return $xml === false ? null : trim((string) $xml->version); +} + +function fail($message) +{ + echo (getenv('GITHUB_ACTIONS') === 'true' ? '::error::' : 'ERROR: ').$message."\n"; +} + +$versions = array( + 'bfstop.xml' => manifestVersion($root.'/bfstop.xml'), + 'package/pkg_bfstop.xml' => manifestVersion($root.'/package/pkg_bfstop.xml'), +); +$update = @simplexml_load_file($root.'/package/update.xml'); +$versions['package/update.xml'] = $update === false ? null : (string) $update->extension['version']; + +$changelogDate = null; +foreach (file($root.'/CHANGELOG', FILE_IGNORE_NEW_LINES) as $line) +{ + if (trim($line) === '') + { + continue; + } + // the first line which isn't empty is the newest entry + if (preg_match('/^(\S+) \((.+)\)$/', $line, $match)) + { + $versions['CHANGELOG'] = $match[1]; + $changelogDate = $match[2]; + } + else + { + $versions['CHANGELOG'] = null; + } + break; +} + +$componentRoot = getenv('COM_BFSTOP_ROOT'); +if ($componentRoot) +{ + $versions['com_bfstop bfstop.xml'] = manifestVersion($componentRoot.'/bfstop.xml'); +} + +$errors = 0; +$expected = $release ?? $versions['bfstop.xml']; +foreach ($versions as $where => $version) +{ + if ($version === null || $version === '') + { + fail("no version found in $where"); + ++$errors; + } + elseif ($version !== $expected) + { + fail("$where has version $version, expected $expected".($release !== null ? ' (the tag)' : ' (as in bfstop.xml)')); + ++$errors; + } +} +if ($release !== null && $changelogDate !== null && + (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $changelogDate) || strtotime($changelogDate) === false)) +{ + fail("the CHANGELOG entry of $release has no release date: '$changelogDate' (write it as YYYY-MM-DD)"); + ++$errors; +} + +echo count($versions)." versions checked".($release !== null ? " against release $release" : '').", $errors problem(s)\n"; +exit($errors > 0 ? 1 : 0); From 92435a8508c8deb251f35257c13a4c4c140d841b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 10:52:28 +0000 Subject: [PATCH 2/2] CI: install the package, and update a site of the previous release with it - A "Package" job builds plugin, component and package with package/build.sh and keeps them as an artifact of the run - a snapshot to try out, and what the other jobs install. - The integration sites install that package through Joomla's installer, the way users do (tests/ci/install-joomla.sh: $BFSTOP_PACKAGE, or the package built from the checkouts, which the com_bfstop workflow uses), instead of plugin and component zipped from the checkouts. - "Update from the previous release" (tests/ci/upgrade-test.sh, on the Joomla 5 sites with MySQL/MariaDB): installs the release named in tests/ci/previous-release.sha256 (the package of the GitHub release, checked against its SHA-256), gives it settings and data the way that version kept them, updates it with the package and runs the new "upgrade" test suite: versions and schema, migrated settings, utf8mb4 tables and username statistics, converted addresses with the blocks still working, an update which stopped halfway run again, and the package refusing a Joomla version it doesn't support. The suite is skipped elsewhere. - The lint job checks that the versions agree. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AFJFMMUpch4LGexEiXN8Nc --- .github/workflows/ci.yml | 127 ++++++++++++++++++--- tests/README.md | 33 +++++- tests/Upgrade/UpgradeTest.php | 190 +++++++++++++++++++++++++++++++ tests/Upgrade/fixtures/seed.php | 48 ++++++++ tests/ci/install-joomla.sh | 30 +++-- tests/ci/previous-release.sha256 | 7 ++ tests/ci/upgrade-test.sh | 45 ++++++++ tests/phpunit.xml.dist | 3 + 8 files changed, 453 insertions(+), 30 deletions(-) create mode 100644 tests/Upgrade/UpgradeTest.php create mode 100644 tests/Upgrade/fixtures/seed.php create mode 100644 tests/ci/previous-release.sha256 create mode 100755 tests/ci/upgrade-test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2010292..82608b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,8 @@ jobs: run: php tests/lint/check-joomla-checksums.php - name: Language files run: php tests/lint/check-language.php bfstop.xml PLG_SYSTEM_BFSTOP + - name: Plugin, package and CHANGELOG have the same version + run: php tests/lint/check-versions.php - name: Composer manifest is valid run: composer validate --strict - name: Vendored GeoIP reader is the version locked in composer.lock @@ -73,29 +75,26 @@ jobs: runs-on: ubuntu-latest outputs: matrix: ${{ steps.read.outputs.matrix }} + upgrade: ${{ steps.read.outputs.upgrade }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - id: read run: | echo "matrix=$(jq -c '{include: .include}' tests/ci/matrix.json)" >> "$GITHUB_OUTPUT" + # the update from the previous release: the Joomla 5 entries with + # MySQL/MariaDB (1.5 ran on neither Joomla 6 nor PostgreSQL) + echo "upgrade=$(jq -c '{include: [.include[] | select((.joomla | startswith("5.")) and (.db | startswith("postgres") | not))]}' tests/ci/matrix.json)" >> "$GITHUB_OUTPUT" - integration: - name: Joomla ${{ matrix.joomla }}, ${{ matrix.db }}, PHP ${{ matrix.php }} - needs: matrix + # what users install: the package with plugin and component, built once by + # package/build.sh; the jobs below install exactly this, and it is kept as + # an artifact of the run (a snapshot to try out) + package: + name: Package runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} - env: - JOOMLA_VERSION: ${{ matrix.joomla }} - DB_HOST: 127.0.0.1 - DB_NAME: joomla - DB_PASS: bfstop-test + outputs: + com-ref: ${{ steps.com.outputs.ref }} + version: ${{ steps.build.outputs.version }} steps: - - name: Set paths - run: | - echo "JOOMLA_ROOT=$RUNNER_TEMP/joomla" >> "$GITHUB_ENV" - echo "COM_BFSTOP_ROOT=$RUNNER_TEMP/com_bfstop" >> "$GITHUB_ENV" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # use the com_bfstop branch of the same name if there is one and it @@ -123,8 +122,53 @@ jobs: repository: codeling/com_bfstop ref: ${{ steps.com.outputs.ref }} path: com_bfstop + - name: Move com_bfstop checkout out of the plugin's + run: mv com_bfstop "$RUNNER_TEMP/com_bfstop" + - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 + with: + php-version: '8.4' + coverage: none + - name: Build plugin, component and package + id: build + run: | + package/build.sh "$RUNNER_TEMP/com_bfstop" build + echo "version=$(php tests/lint/check-versions.php --print-version)" >> "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: bfstop-${{ steps.build.outputs.version }} + path: build/ + if-no-files-found: error + + integration: + name: Joomla ${{ matrix.joomla }}, ${{ matrix.db }}, PHP ${{ matrix.php }} + needs: [matrix, package] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} + env: + JOOMLA_VERSION: ${{ matrix.joomla }} + DB_HOST: 127.0.0.1 + DB_NAME: joomla + DB_PASS: bfstop-test + steps: + - name: Set paths + run: | + echo "JOOMLA_ROOT=$RUNNER_TEMP/joomla" >> "$GITHUB_ENV" + echo "COM_BFSTOP_ROOT=$RUNNER_TEMP/com_bfstop" >> "$GITHUB_ENV" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # the component's classes are loaded from its checkout by the tests + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: codeling/com_bfstop + ref: ${{ needs.package.outputs.com-ref }} + path: com_bfstop - name: Move com_bfstop checkout out of the plugin's run: mv com_bfstop "$COM_BFSTOP_ROOT" + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: bfstop-${{ needs.package.outputs.version }} + path: ${{ runner.temp }}/build - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: @@ -138,7 +182,9 @@ jobs: DB_IMAGE: ${{ matrix.db }} run: tests/ci/start-database.sh - - name: Install Joomla and bfstop + - name: Install Joomla and the bfstop package + env: + BFSTOP_PACKAGE: ${{ runner.temp }}/build/pkg_bfstop-${{ needs.package.outputs.version }}.zip run: tests/ci/install-joomla.sh - name: Tests @@ -147,3 +193,52 @@ jobs: - name: Plugin log if: failure() run: cat "$JOOMLA_ROOT"/administrator/logs/*.php || true + + # a site with the previous release (tests/ci/previous-release.sha256) and + # its data, updated with the package: tests/ci/upgrade-test.sh + upgrade: + name: Update from the previous release (Joomla ${{ matrix.joomla }}, ${{ matrix.db }}) + needs: [matrix, package] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix.outputs.upgrade) }} + env: + JOOMLA_VERSION: ${{ matrix.joomla }} + DB_HOST: 127.0.0.1 + DB_NAME: joomla + DB_PASS: bfstop-test + steps: + - name: Set paths + run: | + echo "JOOMLA_ROOT=$RUNNER_TEMP/joomla" >> "$GITHUB_ENV" + echo "COM_BFSTOP_ROOT=$RUNNER_TEMP/com_bfstop" >> "$GITHUB_ENV" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: codeling/com_bfstop + ref: ${{ needs.package.outputs.com-ref }} + path: com_bfstop + - name: Move com_bfstop checkout out of the plugin's + run: mv com_bfstop "$COM_BFSTOP_ROOT" + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: bfstop-${{ needs.package.outputs.version }} + path: ${{ runner.temp }}/build + - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 + with: + php-version: ${{ matrix.php }} + extensions: mysqli, pdo_mysql, zip, mbstring, intl, gd + tools: phpunit:11 + coverage: none + - name: Start database (${{ matrix.db }}) + env: + DB_IMAGE: ${{ matrix.db }} + run: tests/ci/start-database.sh + - name: Update from the previous release + env: + BFSTOP_PACKAGE: ${{ runner.temp }}/build/pkg_bfstop-${{ needs.package.outputs.version }}.zip + run: tests/ci/upgrade-test.sh + - name: Plugin log + if: failure() + run: cat "$JOOMLA_ROOT"/administrator/logs/*.php || true diff --git a/tests/README.md b/tests/README.md index cac6b2e..e688f6f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -19,7 +19,18 @@ The classes under test are always loaded from this checkout (and the component checkout), not from the copies installed into the Joomla site, so the site only needs to be set up again when the database schema changes. -- `lint/`: checks of the language files (`check-language.php`), of the +- `Upgrade/`: the update from the previous release, run by + `ci/upgrade-test.sh` only (skipped otherwise): a Joomla site with the + release named in `ci/previous-release.sha256` (downloaded from the GitHub + release and checked against that SHA-256), given settings and data in the + form that version kept them (`Upgrade/fixtures/seed.php`), is updated with + the package (`$BFSTOP_PACKAGE`). The tests check the versions and schema, + the migrated settings, the converted tables and addresses, that blocks + still work, that an update which stopped halfway can be run again, and that + the package refuses a Joomla version it doesn't support. After a release, + put the new one into `ci/previous-release.sha256` (the file says how). +- `lint/`: checks of the versions (`check-versions.php`, see + `package/README.md`), of the language files (`check-language.php`), of the release zip built by `deploy.sh zip` (`check-zip.php`) and of the GitHub Actions workflows (`check-workflows.php`: every action pinned to a commit SHA, with its release in a comment; Dependabot proposes the updates); the @@ -29,7 +40,11 @@ the site only needs to be set up again when the database schema changes. run by phpunit or CI. GitHub Actions runs everything on each push and pull request, on Joomla 5 and -6 with MySQL, MariaDB and PostgreSQL, see `.github/workflows/ci.yml`. +6 with MySQL, MariaDB and PostgreSQL, see `.github/workflows/ci.yml`. The +sites there have bfstop installed from the package which the "Package" job +built with `package/build.sh` - the zip users would get, kept as an artifact +of the run - and the "Update from the previous release" jobs run +`ci/upgrade-test.sh` with it, on the Joomla 5 sites with MySQL/MariaDB. Plugin and component are tested together, and each repository's workflow checks out the other one: the branch of the same name if that exists **and has commits @@ -75,8 +90,9 @@ Unit tests only: For the integration tests, create an empty database, then let `tests/ci/install-joomla.sh` set up a Joomla site with bfstop in it (it -downloads Joomla, installs it, then installs plugin and component from the -checkouts). The download is only unpacked if its SHA-256 is the one in +downloads Joomla, installs it, then builds the package from the plugin and +component checkouts with `package/build.sh` and installs it - or the package +zip given in `BFSTOP_PACKAGE`). The download is only unpacked if its SHA-256 is the one in `tests/ci/joomla-checksums.sha256`; for a version that isn't listed, add it there (the file says how to check it) or pass the hash as `JOOMLA_SHA256`. **Everything in that database will be overwritten.** For example, with PostgreSQL: @@ -90,3 +106,12 @@ with PostgreSQL: `DB_TYPE` is `mysqli` for MySQL/MariaDB. The tests empty the bfstop tables and change the plugin's settings (restoring them afterwards), so don't point `JOOMLA_ROOT` at a site you care about. + +The update test needs an empty MySQL/MariaDB database and a directory without +a Joomla site in it, and the package to update to: + + package/build.sh /path/to/com_bfstop + export JOOMLA_VERSION=5.4.10 JOOMLA_ROOT=/tmp/bfstop-upgrade \ + COM_BFSTOP_ROOT=/path/to/com_bfstop BFSTOP_PACKAGE=build/pkg_bfstop-2.0.0.zip \ + DB_TYPE=mysqli DB_HOST=127.0.0.1 DB_USER=root DB_PASS=secret DB_NAME=bfstop_upgrade + tests/ci/upgrade-test.sh diff --git a/tests/Upgrade/UpgradeTest.php b/tests/Upgrade/UpgradeTest.php new file mode 100644 index 0000000..2367991 --- /dev/null +++ b/tests/Upgrade/UpgradeTest.php @@ -0,0 +1,190 @@ +version; + } + + private static function latestSchema() + { + $versions = array_map(function ($file) { return basename($file, '.sql'); }, + glob(self::root().'/sql/updates/*.sql')); + usort($versions, 'version_compare'); + return end($versions); + } + + private function pluginId() + { + return (int) $this->queryValue("SELECT extension_id FROM #__extensions WHERE type='plugin' AND element='bfstop'"); + } + + private function schemaVersion() + { + return $this->queryValue('SELECT version_id FROM #__schemas WHERE extension_id='.$this->pluginId()); + } + + private function installedVersions() + { + $this->db->setQuery("SELECT type, manifest_cache FROM #__extensions WHERE ". + "(type='plugin' AND element='bfstop') OR (type='component' AND element='com_bfstop') OR ". + "(type='package' AND element='pkg_bfstop')"); + $versions = array(); + foreach ($this->db->loadObjectList() as $row) + { + $versions[$row->type] = json_decode($row->manifest_cache, true)['version'] ?? null; + } + ksort($versions); + return $versions; + } + + /** runs Joomla's CLI installer on a package, returns its exit code */ + private function install($package, &$output = null) + { + exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg(getenv('JOOMLA_ROOT').'/cli/joomla.php'). + ' extension:install -n --path='.escapeshellarg($package).' 2>&1', $lines, $exitCode); + $output = implode("\n", $lines); + return $exitCode; + } + + /** a request to the site from $ip, see Integration/fixtures/request.php */ + private function request($ip) + { + exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg(self::root().'/tests/Integration/fixtures/request.php'). + ' '.escapeshellarg($ip).' "" 2>&1', $lines, $exitCode); + $this->assertSame(0, $exitCode, implode("\n", $lines)); + return implode("\n", $lines); + } + + public function testEverythingIsUpdated() + { + $version = self::newVersion(); + $this->assertSame(array('component' => $version, 'package' => $version, 'plugin' => $version), + $this->installedVersions()); + $this->assertSame(self::latestSchema(), $this->schemaVersion()); + $this->assertSame(1, (int) $this->queryValue("SELECT enabled FROM #__extensions WHERE type='plugin' AND element='bfstop'")); + } + + public function testUpdateMessages() + { + // the CLI wraps the messages + $log = preg_replace('/\s+/', ' ', (string) file_get_contents(getenv('UPGRADE_LOG'))); + $settingsMoved = version_compare(getenv('UPGRADE_FROM'), '2.0.0', '<') ? 1 : 0; + $this->assertSame($settingsMoved, substr_count($log, 'major configuration changes'), + 'the "configuration has moved" hint, only when updating from before 2.0.0'); + $this->assertSame(0, substr_count($log, 'protects your site with its default settings'), + 'the hint of a new installation'); + } + + public function testSettingsAreKeptAndMigrated() + { + $params = json_decode($this->getPluginParams(), true); + $this->assertSame('full', $params['blockMode'] ?? null, 'blockEnabled/useHtaccess became blockMode'); + $this->assertSame('15', (string) $params['blockNumber']); + $this->assertSame('90', (string) $params['delayDuration']); + } + + public function testTablesAreConverted() + { + $this->db->setQuery('SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES '. + 'WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE '.$this->db->quote($this->db->getPrefix().'bfstop%')); + $collations = $this->db->loadAssocList('TABLE_NAME', 'TABLE_COLLATION'); + $this->assertCount(count(self::Tables), $collations); + foreach ($collations as $table => $collation) + { + $this->assertStringStartsWith('utf8mb4', $collation, $table); + } + $this->db->setQuery('SELECT username, attempts FROM #__bfstop_username_stats ORDER BY attempts DESC, username'); + $stats = $this->db->loadAssocList('username', 'attempts'); + $this->assertCount(3, $stats, 'Bob and bob are one user name'); + $this->assertSame(array(2, 2, 1), array_map('intval', array_values($stats))); + } + + public function testFirstRequestConvertsTheStoredAddresses() + { + $this->request('192.0.2.200'); + $this->db->setQuery('SELECT ipaddress FROM #__bfstop_bannedip ORDER BY id'); + $this->assertSame(array('2001:db8::5', '203.0.113.0/24', '198.51.100.9'), $this->db->loadColumn()); + $this->assertSame('abcd::1', $this->queryValue('SELECT ipaddress FROM #__bfstop_allowlist')); + $this->assertSame(1, json_decode($this->getPluginParams(), true)['addressesCanonical'] ?? null); + + // and what was blocked stays blocked + $this->assertStringContainsString('STATUS: 403', $this->request('2001:db8::5')); + $this->assertStringContainsString('STATUS: 403', $this->request('203.0.113.99')); + $this->assertStringContainsString('NOT BLOCKED', $this->request('192.0.2.201')); + } + + public function testAnUpdateWhichStoppedHalfwayCanBeRepeated() + { + // the schema version stays at the previous one if the update SQL + // stops; the next attempt runs it again on tables it already changed + $this->db->setQuery("UPDATE #__schemas SET version_id='1.4.3' WHERE extension_id=".$this->pluginId()); + $this->db->execute(); + $this->assertSame(0, $this->install(getenv('BFSTOP_PACKAGE'), $output), $output); + $this->assertSame(self::latestSchema(), $this->schemaVersion()); + } + + public function testPackageRefusesJoomlaVersionsItDoesNotSupport() + { + // the same package, except that it needs a Joomla version which + // doesn't exist yet: the installation has to stop before it changes + // anything + // first the package as it is: its installation has to work now, so + // that a failure below is the version check, not anything else + $this->assertSame(0, $this->install(getenv('BFSTOP_PACKAGE'), $output), $output); + $copy = sys_get_temp_dir().'/bfstop-upgrade-test-'.getmypid().'.zip'; + copy(getenv('BFSTOP_PACKAGE'), $copy); + try + { + $zip = new \ZipArchive(); + $this->assertTrue($zip->open($copy)); + $script = $zip->getFromName('script.php'); + $this->assertMatchesRegularExpression('/\$minimumJoomla = \'[0-9.]+\'/', $script); + $zip->addFromString('script.php', preg_replace('/\$minimumJoomla = \'[0-9.]+\'/', "\$minimumJoomla = '99.0.0'", $script)); + $zip->close(); + + $before = $this->installedVersions(); + $this->assertNotSame(0, $this->install($copy, $output), $output); + $this->assertSame($before, $this->installedVersions()); + } + finally + { + @unlink($copy); + } + } +} diff --git a/tests/Upgrade/fixtures/seed.php b/tests/Upgrade/fixtures/seed.php new file mode 100644 index 0000000..467f25c --- /dev/null +++ b/tests/Upgrade/fixtures/seed.php @@ -0,0 +1,48 @@ +get(DatabaseInterface::class); +$now = date('Y-m-d H:i:s'); + +$params = array( + 'blockEnabled' => '1', + 'useHtaccess' => '0', + 'blockNumber' => '15', + 'delayDuration' => '90', + 'deleteOld' => '0', + 'logLevel' => '8', +); +$db->setQuery('UPDATE #__extensions SET enabled=1, params='.$db->quote(json_encode($params)). + " WHERE type='plugin' AND folder='system' AND element='bfstop'"); +$db->execute(); + +foreach (array('2001:DB8::5', '203.0.113.7/24', '198.51.100.9') as $address) +{ + $row = (object) array('ipaddress' => $address, 'crdate' => $now, 'duration' => 0); + $db->insertObject('#__bfstop_bannedip', $row); +} +$row = (object) array('ipaddress' => 'ABCD::1', 'notes' => 'office'); +$db->insertObject('#__bfstop_allowlist', $row); +foreach (array('admin', 'admin', 'Bob', 'bob', 'jürgen') as $username) +{ + $row = (object) array('username' => $username, 'ipaddress' => '192.0.2.1', + 'logtime' => $now, 'origin' => 0, 'handled' => 0); + $db->insertObject('#__bfstop_failedlogin', $row); +} +echo "Seeded the site with settings and data of the previous release\n"; diff --git a/tests/ci/install-joomla.sh b/tests/ci/install-joomla.sh index b0bf13d..5e33159 100755 --- a/tests/ci/install-joomla.sh +++ b/tests/ci/install-joomla.sh @@ -3,8 +3,14 @@ # Sets up a Joomla site with bfstop installed, for the integration tests: # downloads the Joomla full package (unless $JOOMLA_ROOT already contains a # not yet installed Joomla), runs Joomla's CLI installer against the given -# database, and installs the plugin (and, if $COM_BFSTOP_ROOT is set, the -# component) from the source checkouts through Joomla's extension installer. +# database, and installs bfstop through Joomla's extension installer - the +# way users install it: +# +# - $BFSTOP_PACKAGE set: that package zip (e.g. the one CI built, or the +# previous release for the upgrade test) +# - else, with $COM_BFSTOP_ROOT: the package built from the checkouts by +# package/build.sh +# - else: only the plugin, zipped by its deploy.sh # # Environment: # JOOMLA_VERSION Joomla version to download, e.g. 5.4.8 @@ -12,6 +18,7 @@ # DB_TYPE mysqli or pgsql # DB_HOST, DB_USER, DB_PASS, DB_NAME database connection (database must exist) # COM_BFSTOP_ROOT optional: checkout of https://github.com/codeling/com_bfstop +# BFSTOP_PACKAGE optional: package zip to install instead of building one set -euo pipefail : "${JOOMLA_ROOT:?}" "${DB_TYPE:?}" "${DB_HOST:?}" "${DB_USER:?}" "${DB_NAME:?}" @@ -35,15 +42,18 @@ php "$JOOMLA_ROOT/installation/joomla.php" install -n \ --db-pass="${DB_PASS:-}" --db-name="$DB_NAME" --db-prefix=jos_ \ --db-encryption=0 --public-folder="" -install_extension() { - local src="$1" zip="$build_dir/$2.zip" - (cd "$src" && zip -qr "$zip" . -x '.git/*' '.github/*' 'tests/*') - echo "Installing $2" - php "$JOOMLA_ROOT/cli/joomla.php" extension:install --path="$zip" -n +install_zip() { + echo "Installing $(basename "$1")" + php "$JOOMLA_ROOT/cli/joomla.php" extension:install --path="$1" -n } -install_extension "$plugin_root" plg_system_bfstop -if [ -n "${COM_BFSTOP_ROOT:-}" ]; then - install_extension "$COM_BFSTOP_ROOT" com_bfstop +if [ -n "${BFSTOP_PACKAGE:-}" ]; then + install_zip "$BFSTOP_PACKAGE" +elif [ -n "${COM_BFSTOP_ROOT:-}" ]; then + "$plugin_root/package/build.sh" "$COM_BFSTOP_ROOT" "$build_dir" > /dev/null + install_zip "$build_dir"/pkg_bfstop-*.zip +else + (cd "$plugin_root" && ./deploy.sh zip > /dev/null && mv bfstop-*.zip "$build_dir/") + install_zip "$build_dir"/bfstop-*.zip fi rm -rf "$build_dir" diff --git a/tests/ci/previous-release.sha256 b/tests/ci/previous-release.sha256 new file mode 100644 index 0000000..d1294c6 --- /dev/null +++ b/tests/ci/previous-release.sha256 @@ -0,0 +1,7 @@ +# The release the upgrade test (tests/ci/upgrade-test.sh) updates from: the +# package as published on https://github.com/codeling/bfstop/releases, with +# its SHA-256 - what the test installs is code from that download, so it has +# to be the package that was checked. After a release, put the new one here: +# curl -sSLO https://github.com/codeling/bfstop/releases/download//pkg_bfstop-.zip +# sha256sum pkg_bfstop-.zip +1bd2fa5a8f5aac10e608d2ad319d2fe3d5e8d612455e3c015e69a7d01fa26f45 pkg_bfstop-1.5.2.zip diff --git a/tests/ci/upgrade-test.sh b/tests/ci/upgrade-test.sh new file mode 100755 index 0000000..b0462c3 --- /dev/null +++ b/tests/ci/upgrade-test.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# Tests the update from the previous release (tests/ci/previous-release.sha256) +# to the package in $BFSTOP_PACKAGE, the way a site is updated: sets up a +# Joomla site with the previous release, gives it settings and data in the +# form that version kept them (tests/Upgrade/fixtures/seed.php), installs the +# new package over it and runs the "upgrade" test suite on the result. +# +# Environment: as for install-joomla.sh (the database must be MySQL/MariaDB: +# PostgreSQL is supported from 2.0.0 on), plus +# BFSTOP_PACKAGE the package zip to update to +# COM_BFSTOP_ROOT checkout of com_bfstop (the tests load its classes) +# PHPUNIT optional: the phpunit command (default: phpunit) +set -euo pipefail + +: "${BFSTOP_PACKAGE:?}" "${JOOMLA_ROOT:?}" +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +plugin_root="$(cd "$here/../.." && pwd)" +new_package="$(cd "$(dirname "$BFSTOP_PACKAGE")" && pwd)/$(basename "$BFSTOP_PACKAGE")" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +sha256() { + if command -v sha256sum > /dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi +} + +read -r expected file < <(grep -v '^#' "$here/previous-release.sha256" | grep . | head -n 1) +previous="${file#pkg_bfstop-}" +previous="${previous%.zip}" +echo "Downloading BFStop $previous" +curl -fsSL -o "$work/$file" "https://github.com/codeling/bfstop/releases/download/$previous/$file" +actual="$(sha256 "$work/$file")" +if [ "$actual" != "$expected" ]; then + echo "SHA-256 of $file differs: expected $expected, got $actual" >&2 + exit 1 +fi + +BFSTOP_PACKAGE="$work/$file" "$here/install-joomla.sh" +php "$plugin_root/tests/Upgrade/fixtures/seed.php" + +echo "Updating to $(basename "$new_package")" +php "$JOOMLA_ROOT/cli/joomla.php" extension:install --path="$new_package" -n | tee "$work/update.log" + +UPGRADE_FROM="$previous" UPGRADE_LOG="$work/update.log" BFSTOP_PACKAGE="$new_package" \ + "${PHPUNIT:-phpunit}" -c "$plugin_root/tests/phpunit.xml.dist" --testsuite upgrade diff --git a/tests/phpunit.xml.dist b/tests/phpunit.xml.dist index 5752431..4fb9f45 100644 --- a/tests/phpunit.xml.dist +++ b/tests/phpunit.xml.dist @@ -15,5 +15,8 @@ Integration + + Upgrade +