From d0cf38c8a6d41aea79c36de0d16d893b25ac7f7b Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Wed, 29 Jul 2026 10:33:10 +0200 Subject: [PATCH] Harden upstream ImapTest adapter --- contrib/testing/README.md | 4 ++- contrib/testing/upstream-imaptest-check.py | 39 +++++++++++++++++----- docs/upstream-test-suites.md | 4 +++ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/contrib/testing/README.md b/contrib/testing/README.md index 4ccdd88..aab80b9 100644 --- a/contrib/testing/README.md +++ b/contrib/testing/README.md @@ -66,7 +66,9 @@ one-off files in `/tmp`: tracking. POP mode seeds three identifiable messages through Bongo IMAPS, runs ImapTest's POP3 profile with message retention, and verifies and removes all three fixtures afterward. Credentials are passed through a mode-0600 - temporary user file instead of the process command line. See + temporary user file instead of the process command line. A scripted group + can be isolated with repeatable `--test-group NAME`; raw protocol logging is + deliberately disabled because it can expose the decoded login. See `docs/upstream-test-suites.md` for the limits of upstream-suite reuse. - `smtp4dev-fixture.py` manages two authenticated, loopback-only external SMTP provider fixtures for STARTTLS and implicit TLS plus a TLS-required, diff --git a/contrib/testing/upstream-imaptest-check.py b/contrib/testing/upstream-imaptest-check.py index ba48fbf..b0086ac 100755 --- a/contrib/testing/upstream-imaptest-check.py +++ b/contrib/testing/upstream-imaptest-check.py @@ -89,9 +89,16 @@ def find_source(explicit: str | None) -> Path: return source -def selected_tests(source: Path, suite: str) -> tuple[Path, ...]: +def selected_tests( + source: Path, suite: str, explicit: tuple[str, ...] = () +) -> tuple[Path, ...]: source_tests = source / "src" / "tests" - if suite == "core": + if explicit: + for name in explicit: + if not name or Path(name).name != name or name.endswith(".mbox"): + raise UpstreamTestError(f"invalid upstream test group: {name!r}") + names = explicit + elif suite == "core": names = CORE_TESTS elif suite == "search": names = SEARCH_TESTS @@ -113,9 +120,14 @@ def selected_tests(source: Path, suite: str) -> tuple[Path, ...]: return paths -def stage_tests(source: Path, suite: str, destination: Path) -> int: +def stage_tests( + source: Path, + suite: str, + destination: Path, + explicit: tuple[str, ...] = (), +) -> int: source_tests = source / "src" / "tests" - paths = selected_tests(source, suite) + paths = selected_tests(source, suite, explicit) shutil.copy2(source_tests / "default.mbox", destination / "default.mbox") for path in paths: shutil.copy2(path, destination / path.name) @@ -142,7 +154,6 @@ def common_arguments(args: argparse.Namespace, userfile: Path) -> list[str]: f"port={args.port}", f"userfile={userfile}", "ssl=any-cert" if not args.ca_file else "ssl", - "disconnect_quit", ] if args.ca_file: command.append(f"ssl_ca_file={Path(args.ca_file).resolve()}") @@ -180,13 +191,15 @@ def scripted(args: argparse.Namespace) -> None: directory = Path(raw) staged = directory / "tests" staged.mkdir() - count = stage_tests(source, args.suite, staged) + explicit = tuple(args.test_group) + count = stage_tests(source, args.suite, staged, explicit) userfile = credentials_file(directory, args.username, args.password) command = common_arguments(args, userfile) - command.extend((f"box={args.mailbox}", f"test={staged}", "rawlog")) + command.extend((f"box={args.mailbox}", f"test={staged}")) run(command, args.timeout, directory) + label = ", ".join(explicit) if explicit else f"{args.suite} suite" print( - f"PASS: Dovecot ImapTest {args.suite} suite " + f"PASS: Dovecot ImapTest {label} " f"({count} scripted test groups)" ) @@ -208,6 +221,7 @@ def stress(args: argparse.Namespace) -> None: "random", "own_msgs", "own_flags", + "disconnect_quit", "stalled_disconnect_timeout=15", ) ) @@ -384,6 +398,15 @@ def parser() -> argparse.ArgumentParser: result.add_argument("--ca-file", default=os.environ.get("BONGO_TEST_CA_FILE")) result.add_argument("--imap4rev2", action="store_true") result.add_argument("--suite", choices=("core", "search", "all"), default="core") + result.add_argument( + "--test-group", + action="append", + default=[], + help=( + "run one named upstream scripted group; may be repeated and " + "overrides --suite" + ), + ) result.add_argument("--clients", type=int, default=4) result.add_argument("--seconds", type=int, default=30) result.add_argument("--checkpoint", type=int, default=5) diff --git a/docs/upstream-test-suites.md b/docs/upstream-test-suites.md index 9aef5d0..f0bf984 100644 --- a/docs/upstream-test-suites.md +++ b/docs/upstream-test-suites.md @@ -47,6 +47,10 @@ export BONGO_TEST_PASSWORD='a-disposable-password-of-at-least-12-characters' The complete upstream suite can be selected with `--suite all`. Missing optional capabilities are reported as skips by ImapTest; a failure in the base protocol or an advertised extension fails the Bongo test. +For diagnosis, `--test-group append` selects one named upstream file and may +be repeated. The adapter does not enable ImapTest's raw logging because a raw +IMAP trace can contain the decoded test password; the captured test summary +is the release artifact. The upstream scripted corpus uses IMAP4rev1 response rules, including `RECENT`. It must not be combined with `--imap4rev2`. Bongo's dedicated live