- tests/Support/FakeChangeLog extends ChangeLog: records record()/recordPartial() calls, increasing ids, optional failure; new FakeChangeLog(id) / partialsOnly() / any(id) / forbidden() mirror what the replaced mocks allowed. assertRecorded() / assertPartials() check count and the named arguments. Calls a fake does not allow throw and are also verified in TestCase::tearDown (one assertion per test that uses a fake), so swallowed change-log calls still fail like a never() expectation. - All ChangeLog mocks of ability/integration tests replaced (the admin tests that stub query/purge/clear keep their mocks); expectException tests that checked the partial log now catch and assert. - SchemaFixture::managed(?divi) replaces the 21 isManaged-only ContentSchema mocks; DiviFixtures::schema() uses it. Tests 1397 -> 1404 (FakeChangeLogTest); no test has fewer assertions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
182 lines
5.8 KiB
PHP
182 lines
5.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Support;
|
||
|
||
use LIT\MCP\ChangeLog\ChangeLog;
|
||
use LogicException;
|
||
use PHPUnit\Framework\Assert;
|
||
use Throwable;
|
||
|
||
/**
|
||
* ChangeLog without a database. Allowed calls are kept in $records / $partials (arguments by
|
||
* name); record() returns increasing ids from $firstId, or throws $failure after noting the call.
|
||
*
|
||
* What a fake allows mirrors the Mockery mocks it replaces:
|
||
* - new FakeChangeLog(id) record() only (allows('record')->andReturn(id))
|
||
* - FakeChangeLog::partialsOnly() recordPartial() only (expects('recordPartial'))
|
||
* - FakeChangeLog::any(id) both
|
||
* - FakeChangeLog::forbidden() nothing (a mock without expectations)
|
||
* A call that is not allowed throws – and, because the code under test often swallows
|
||
* change-log failures (FollowUp), it is also noted and fails the test in TestCase::tearDown()
|
||
* via verify().
|
||
*/
|
||
class FakeChangeLog extends ChangeLog
|
||
{
|
||
/**
|
||
* @var list<array{ability: string, object_type: string, object_id: string, summary: string,
|
||
* revision_id: ?int, snapshot_id: ?string, result: string}>
|
||
*/
|
||
public array $records = [];
|
||
|
||
/**
|
||
* @var list<array{ability: string, object_type: string, object_id: string, summary: string,
|
||
* revision_id: ?int}>
|
||
*/
|
||
public array $partials = [];
|
||
|
||
/** @var list<string> Calls that were not allowed, across all fakes of the running test. */
|
||
private static array $unexpected = [];
|
||
|
||
private static bool $used = false;
|
||
|
||
public function __construct(
|
||
private int $firstId = 1,
|
||
private ?Throwable $failure = null,
|
||
private bool $allowsRecord = true,
|
||
private bool $allowsPartial = false
|
||
) {
|
||
parent::__construct();
|
||
self::$used = true;
|
||
}
|
||
|
||
public static function forbidden(): self
|
||
{
|
||
return new self(1, null, false, false);
|
||
}
|
||
|
||
public static function partialsOnly(): self
|
||
{
|
||
return new self(1, null, false, true);
|
||
}
|
||
|
||
public static function any(int $firstId = 1): self
|
||
{
|
||
return new self($firstId, null, true, true);
|
||
}
|
||
|
||
/**
|
||
* Fails the running test if any fake got a call it does not allow; resets for the next test.
|
||
* Counts as one assertion in tests that created a fake (like a Mockery never() expectation).
|
||
*/
|
||
public static function verify(): void
|
||
{
|
||
$unexpected = self::$unexpected;
|
||
$used = self::$used;
|
||
self::$unexpected = [];
|
||
self::$used = false;
|
||
if ($used || $unexpected !== []) {
|
||
Assert::assertSame([], $unexpected, 'Unexpected change log calls: ' . implode('; ', $unexpected));
|
||
}
|
||
}
|
||
|
||
public function record(
|
||
string $ability,
|
||
string $objectType,
|
||
string $objectId,
|
||
string $summary,
|
||
?int $revisionId = null,
|
||
?string $snapshotId = null,
|
||
string $result = 'ok'
|
||
): ?int {
|
||
$this->refuseUnless($this->allowsRecord, sprintf('record(%s, %s, %s)', $ability, $objectType, $objectId));
|
||
$this->records[] = [
|
||
'ability' => $ability,
|
||
'object_type' => $objectType,
|
||
'object_id' => $objectId,
|
||
'summary' => $summary,
|
||
'revision_id' => $revisionId,
|
||
'snapshot_id' => $snapshotId,
|
||
'result' => $result,
|
||
];
|
||
if ($this->failure !== null) {
|
||
throw $this->failure;
|
||
}
|
||
|
||
return $this->firstId + count($this->records) - 1;
|
||
}
|
||
|
||
public function recordPartial(
|
||
string $ability,
|
||
string $objectType,
|
||
string $objectId,
|
||
string $summary,
|
||
?int $revisionId = null
|
||
): void {
|
||
$this->refuseUnless(
|
||
$this->allowsPartial,
|
||
sprintf('recordPartial(%s, %s, %s)', $ability, $objectType, $objectId)
|
||
);
|
||
$this->partials[] = [
|
||
'ability' => $ability,
|
||
'object_type' => $objectType,
|
||
'object_id' => $objectId,
|
||
'summary' => $summary,
|
||
'revision_id' => $revisionId,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Asserts the record() calls: one $expected per call, in order; each names only the
|
||
* arguments it checks (ability, object_type, object_id, summary, revision_id, …).
|
||
*
|
||
* @param array<string, mixed> ...$expected
|
||
*/
|
||
public function assertRecorded(array ...$expected): void
|
||
{
|
||
self::assertCalls('record()', $expected, $this->records);
|
||
}
|
||
|
||
/**
|
||
* Asserts the recordPartial() calls like assertRecorded().
|
||
*
|
||
* @param array<string, mixed> ...$expected
|
||
*/
|
||
public function assertPartials(array ...$expected): void
|
||
{
|
||
self::assertCalls('recordPartial()', $expected, $this->partials);
|
||
}
|
||
|
||
/**
|
||
* @param list<array<string, mixed>> $expected
|
||
* @param list<array<string, mixed>> $actual
|
||
*/
|
||
private static function assertCalls(string $method, array $expected, array $actual): void
|
||
{
|
||
Assert::assertCount(count($expected), $actual, 'Number of ChangeLog::' . $method . ' calls');
|
||
foreach ($expected as $i => $call) {
|
||
$checked = [];
|
||
foreach (array_keys($call) as $key) {
|
||
Assert::assertArrayHasKey($key, $actual[$i], 'Unknown ChangeLog argument');
|
||
$checked[$key] = $actual[$i][$key];
|
||
}
|
||
Assert::assertSame($call, $checked, 'ChangeLog::' . $method . ' call ' . $i);
|
||
}
|
||
}
|
||
|
||
/** @return list<string> The summaries of record(), in order. */
|
||
public function summaries(): array
|
||
{
|
||
return array_column($this->records, 'summary');
|
||
}
|
||
|
||
private function refuseUnless(bool $allowed, string $call): void
|
||
{
|
||
if (! $allowed) {
|
||
self::$unexpected[] = $call;
|
||
throw new LogicException('Unexpected ChangeLog::' . $call);
|
||
}
|
||
}
|
||
}
|