From 9b1dc82b97a3b22019c21d2026f65f2b279e72b0 Mon Sep 17 00:00:00 2001 From: "v.razuvaev" Date: Thu, 20 Aug 2026 21:01:02 +0300 Subject: [PATCH] Reject a non-redis PROPERTY_DB scheme instead of silently writing to a directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only an exact redis:// prefix was recognised; a rediss:// typo — or any other scheme — fell through to FilesystemCorpus and silently wrote the corpus to a directory named after the scheme, the exact silent filesystem fall-back the design forbids. Match the scheme case-insensitively (Redis:// is a shared corpus) and error on any non-redis scheme, naming the scheme but not the DSN (which may carry credentials). Fixes #13 --- CHANGELOG.md | 11 +++++++++++ README.md | 4 +++- README.ru.md | 4 +++- src/PhpUnit/CorpusFromEnv.php | 19 ++++++++++++++++-- tests/CorpusFromEnvTest.php | 36 +++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 388a9d7..7cbba9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.5.1 — 2026-08-20 + +- `PROPERTY_DB` with a non-`redis` URI scheme is now a configuration error + instead of a directory named after the scheme. Only an exact `redis://` + prefix was recognised, so a `rediss://` typo — or any other scheme — fell + through to `FilesystemCorpus` and silently wrote the corpus to a directory + nobody reads, exactly the "silent fall back to the filesystem" the design + forbids. Scheme matching is now case-insensitive (`Redis://` is a shared + corpus) and the error names the scheme but not the DSN, which may carry + credentials. A path with no scheme is unchanged. + ## 0.5.0 — 2026-08-16 - Added fluent `auto()`: a generator is derived from the property closure's diff --git a/README.md b/README.md index 877da0d..0227f6f 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,9 @@ PROPERTY_DB=redis://redis:6379/suite-a: vendor/bin/phpunit # shared server, o A directory remembers a counterexample for whoever owns it — in CI, a machine deleted when the job ends. The Redis form is the same corpus, in the same document, shared. It needs `ext-redis` or `predis/predis`; neither installed is -an error rather than a silent fall back to the filesystem. +an error rather than a silent fall back to the filesystem. A `PROPERTY_DB` with +any other scheme — a `rediss://` typo, another backend — is likewise an error, +never a directory named after the scheme. The corpus format is exactly the one `rasuvaeff/property-testing` 2.8 wrote — a corpus recorded under Testo (or under 2.x) replays here and vice versa. On diff --git a/README.ru.md b/README.ru.md index 48babac..3d9f6f7 100644 --- a/README.ru.md +++ b/README.ru.md @@ -230,7 +230,9 @@ PROPERTY_DB=redis://redis:6379/suite-a: vendor/bin/phpunit # общий се Каталог помнит контрпример для того, кто им владеет, — в CI это машина, которую удаляют вместе с job'ом. Redis-форма — тот же корпус в том же документе, но общий. Нужен `ext-redis` или `predis/predis`; отсутствие обоих — -ошибка, а не тихий откат на файловую систему. +ошибка, а не тихий откат на файловую систему. `PROPERTY_DB` с любой другой +схемой — опечатка `rediss://`, другой бэкенд — тоже ошибка, а не каталог с +именем схемы. Формат корпуса — ровно тот, что писал `rasuvaeff/property-testing` 2.8: корпус, записанный под Testo (или под 2.x), реплеится здесь, и наоборот. При diff --git a/src/PhpUnit/CorpusFromEnv.php b/src/PhpUnit/CorpusFromEnv.php index 2a94087..ccd11ae 100644 --- a/src/PhpUnit/CorpusFromEnv.php +++ b/src/PhpUnit/CorpusFromEnv.php @@ -29,7 +29,12 @@ */ final class CorpusFromEnv { - private const string SCHEME = 'redis://'; + /** + * A leading URI scheme: `redis` in `redis://host`. A value without one is + * a directory path; a value with a scheme that is not `redis` is a typo + * (`rediss://`, `Redis://`) or the wrong backend — never a directory. + */ + private const string SCHEME_PATTERN = '#^([a-zA-Z][a-zA-Z0-9+.\-]*)://#'; private function __construct() { @@ -48,10 +53,20 @@ public static function resolve(): ?Corpus return null; } - if (!str_starts_with($dsn, self::SCHEME)) { + if (preg_match(self::SCHEME_PATTERN, $dsn, $matches) !== 1) { return new FilesystemCorpus($dsn); } + if (strtolower($matches[1]) !== 'redis') { + // Not a directory: a suite told to share its corpus, quietly + // writing to a directory nobody reads, is worse than one that + // stops. The DSN itself is not echoed — it may carry credentials. + throw new \InvalidArgumentException(sprintf( + 'PROPERTY_DB uses an unsupported scheme "%s://"; use redis:// for a shared corpus or a plain directory path for a local one', + $matches[1], + )); + } + $parsed = RedisDsn::parse($dsn); return new RedisCorpus(self::client($parsed, $dsn), $parsed->prefix); diff --git a/tests/CorpusFromEnvTest.php b/tests/CorpusFromEnvTest.php index aef8a49..ca3b1ab 100644 --- a/tests/CorpusFromEnvTest.php +++ b/tests/CorpusFromEnvTest.php @@ -136,4 +136,40 @@ public function testAnUnusableDsnSurfacesAsAConfigurationError(): void $restore(); } } + + /** + * A scheme other than `redis` — a `rediss://` typo, the wrong backend — is + * a configuration error, never a directory named `rediss:`. The message + * names the scheme but not the DSN, which may carry credentials. + */ + public function testAMistypedSchemeErrorsInsteadOfWritingToADirectory(): void + { + $restore = Env::set('PROPERTY_DB', 'rediss://127.0.0.1:6379/suite'); + + try { + CorpusFromEnv::resolve(); + + self::fail('expected an InvalidArgumentException'); + } catch (\InvalidArgumentException $e) { + self::assertStringContainsString('unsupported scheme "rediss://"', $e->getMessage()); + self::assertStringNotContainsString('127.0.0.1', $e->getMessage()); + } finally { + $restore(); + } + } + + /** + * URI schemes are case-insensitive, so `Redis://` is still a shared corpus, + * not a directory the previous exact-string check would have created. + */ + public function testTheRedisSchemeIsMatchedCaseInsensitively(): void + { + $restore = Env::set('PROPERTY_DB', 'Redis://127.0.0.1:6399/suite:'); + + try { + self::assertInstanceOf(RedisCorpus::class, CorpusFromEnv::resolve()); + } finally { + $restore(); + } + } }