Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ though it succeeded — it pauses so a human can read its "N duplicate tables
ignored" report, and the form's `pop_done` field is the short-circuit past it.
Passing `pop_done` on the first pass would skip building the schema entirely.

It then deletes `install.php`, which the installer asks for but cannot do
itself — its `?delete` link is a GET, and command line arguments only ever reach
`$_POST`. That matters more than it sounds: while the file is there
`Settings.php` redirects every request back into the installer, and SMF puts a
"MAJOR SECURITY RISK" box on every page it shows an administrator. Reinstalling
still works, because `reset.sh` runs first and does not return until the
entrypoint has staged a fresh copy.

Two flags worth knowing:

- `--force` reinstalls even when a forum is already there. Without it the
Expand Down Expand Up @@ -102,6 +110,56 @@ are gitignored.
installer, discarding that forum. `use-engine.sh` switches between forums,
`reset.sh` throws one away.

## Accounts and passwords

Two forums, each with its own administrator, and a password chosen months ago is
a recipe for an afternoon of hand written SQL. `user.sh` is there so it is not:

```sh
.docker/user.sh list
.docker/user.sh check admin 'password'
.docker/user.sh reset admin 'a new password'
```

`check` exits 0 when SMF would accept the password and 1 when it would not, so
it works in a conditional as well as by eye. It also points out an account that
is not activated, which fails to log in with a correct password and looks
exactly like a wrong one.

`--engine mysql|postgresql` reads the settings `use-engine.sh` saved for that
engine, so the *other* forum can be inspected without switching to it:

```sh
.docker/user.sh check admin 'password' --engine mysql
```

The hashing goes through SMF's own `Security` class rather than being written
here, so what `reset` puts in the table is by construction what `Login2` expects
to find. It clears `passwd_flood` at the same time: SMF locks an account out for
a while after enough wrong guesses, and a fresh password behind a lockout looks
exactly like a password that did not take.

## Running the tests

```sh
.docker/test.sh # both engines
.docker/test.sh --engine postgresql
.docker/test.sh --engine both --filter ModSettings
```

Anything it does not recognise is passed on to PHPUnit. It installs a forum for
an engine that has not got one, and puts the previously active engine back when
it finishes.

Running on both is the point rather than a thoroughness exercise. The counter
regression in `tests/Integration/ModSettingsTest.php` **passes on MySQL with the
bug still in place** and only fails on PostgreSQL, because MySQL coerces text to
a number where PostgreSQL refuses. A suite that only ever sees one engine proves
considerably less than it looks like it does.

The unit suite needs none of this — `composer test` runs everything, and the
integration tests skip themselves when there is no forum to talk to.

### Installing in a browser instead

On first boot the entrypoint writes a `Settings.php` pre-filled for the chosen
Expand Down Expand Up @@ -247,4 +305,10 @@ compose.yaml the stack
.docker/mysql/init/10-smf.sh runs once on first mysql database creation
.docker/postgres/init/10-smf.sh runs once on first postgres database creation
.docker/env.example optional overrides

.docker/lib.sh paths, credentials and engine names, shared
.docker/install-forum.sh install a forum with no browser involved
.docker/reset.sh empty one engine and restage the installer
.docker/use-engine.sh switch which installed forum is live
.docker/user.sh inspect accounts, check and reset passwords
```
18 changes: 18 additions & 0 deletions .docker/install-forum.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ install_one() {
--password2="$SMF_ADMIN_PASS"
)

# reset.sh does not return until the entrypoint has staged this, so its
# absence means something went wrong there rather than here. Worth saying so:
# without it php reports "Could not open input file: install.php", which reads
# like a broken script rather than a forum that was never made installable.
docker compose exec -T web test -f install.php \
|| die "${smf_type}: install.php is not staged, so there is nothing to run (docker compose logs web)"

log "${smf_type}: building the schema"
docker compose exec -T web php install.php "${args[@]}" >/dev/null

Expand All @@ -107,6 +114,17 @@ install_one() {

[ -n "$version" ] || die "${smf_type}: the installer finished but the forum is not installed"

# The installer tells you to delete this and cannot do it itself: its ?delete
# link is a GET, and command line arguments only ever reach $_POST. Leaving it
# is not cosmetic - Settings.php redirects every request back into the
# installer while it is there, and SMF puts a "MAJOR SECURITY RISK: you have
# not removed install.php" box on every page it shows an administrator.
#
# Safe to delete even though a reinstall needs it again: install_one() always
# calls reset.sh first, and reset.sh clears Settings.php and waits for the
# entrypoint to put a fresh copy back before returning.
rm -f install.php

log "${smf_type}: installed SMF ${version}"

if [ "$PIN_SECRETS" -eq 1 ]; then
Expand Down
81 changes: 81 additions & 0 deletions .docker/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Runs the test suite against a real forum, on one engine or on both.
#
# .docker/test.sh both engines, whole suite
# .docker/test.sh --engine postgresql
# .docker/test.sh --engine both --filter ModSettings
#
# Anything after the recognised options is handed straight to PHPUnit, so
# --filter, --testsuite and friends work as usual.
#
# Installs a forum for an engine that has not got one yet. Use
# .docker/install-forum.sh --force to start any of them over.
#
# Running on both engines is the point rather than a thoroughness exercise: the
# two disagree often enough that a suite which only ever sees one of them
# proves considerably less than it appears to. The counter regression in
# tests/Integration/ModSettingsTest.php passes on MySQL with the bug still in
# place, and fails on PostgreSQL.
#
# Runs on the host.
set -euo pipefail

. "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"

ENGINE='both'
PHPUNIT_ARGS=()

while [ $# -gt 0 ]; do
case "$1" in
--engine) ENGINE="$2"; shift 2 ;;
--engine=*) ENGINE="${1#*=}"; shift ;;
-h|--help) sed -n '2,19p' "${BASH_SOURCE[0]}"; exit 0 ;;
*) PHPUNIT_ARGS+=("$1"); shift ;;
esac
done

ENGINES=$(engine_list "$ENGINE") || die "unknown engine: $ENGINE"

cd "$BOARD_DIR"

# Remember what was active, and put it back afterwards however this ends. A test
# run should not silently leave the forum pointed somewhere else.
ORIGINAL=''

if [ -f Settings.php ]; then
ORIGINAL=$(sed -n "s|^\$db_type = '\([^']*\)';.*|\1|p" Settings.php | head -n 1 | tr '[:upper:]' '[:lower:]')
fi

restore_engine() {
if [ -n "$ORIGINAL" ] && [ -f "$SETTINGS_DIR/Settings.$(engine_smf_type "$ORIGINAL").php" ]; then
"$DOCKER_DIR/use-engine.sh" "$ORIGINAL" >/dev/null 2>&1 || true
fi
}

trap restore_engine EXIT

FAILED=''

for smf_type in $ENGINES; do
if [ -z "$(installed_version "$smf_type" || true)" ]; then
log "${smf_type}: no forum yet, installing one"
"$DOCKER_DIR/install-forum.sh" --engine "$smf_type" >/dev/null
fi

"$DOCKER_DIR/use-engine.sh" "$smf_type" >/dev/null

log "${smf_type}: running the tests"

if docker compose exec -T web vendor/bin/phpunit --no-coverage --colors=always "${PHPUNIT_ARGS[@]+"${PHPUNIT_ARGS[@]}"}"; then
log "${smf_type}: passed"
else
warn "${smf_type}: FAILED"
FAILED="${FAILED} ${smf_type}"
fi
done

if [ -n "$FAILED" ]; then
die "failed on:${FAILED}"
fi

log "passed on: ${ENGINES}"
Loading
Loading