Files
WP-LIT-MCP/tests/Unit/ChangeLog/ChangeLogTest.php
T
janniclas.loosenandClaude Opus 5.5 522f5bbd19 feat: change log tab with filters, revision links and clear button
ChangeLog gains query()/abilities()/clear() (filters: ability, user,
result, date range - all optional and additive). LogTab/LogListTable
render the "Protokoll" admin tab: WP_List_Table with pagination, links
to the post/attachment/nav_menu edit screen by object_type, a revision
link shown only when the user may edit the revision's parent post, and
a nonce+manage_options-gated "Protokoll leeren" with a JS confirmation
and highlighted 'partial' rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-26 04:12:13 +02:00

196 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\ChangeLog;
use Brain\Monkey\Functions;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Server\RequestContext;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
final class ChangeLogTest extends TestCase
{
protected function tearDown(): void
{
RequestContext::reset();
parent::tearDown();
}
public function testRecordStoresReferencesAndReturnsTheId(): void
{
Functions\when('current_time')->justReturn('2026-09-25 08:00:00');
Functions\when('get_current_user_id')->justReturn(3);
RequestContext::reset(true, 'Claude');
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->expects('insert')
->with('wp_lit_mcp_log', Mockery::on(static function (array $row): bool {
return $row['created_at'] === '2026-09-25 08:00:00'
&& $row['user_id'] === 3
&& $row['client'] === 'Claude'
&& $row['ability'] === 'lit/content-update'
&& $row['object_type'] === 'page'
&& $row['object_id'] === '12'
&& $row['summary'] === 'Seite geändert'
&& $row['revision_id'] === 811
&& $row['snapshot_id'] === null
&& $row['result'] === 'ok';
}), Mockery::type('array'))
->andReturnUsing(static function () use ($db): int {
$db->insert_id = 4711;
return 1;
});
$id = (new ChangeLog($db))->record('lit/content-update', 'page', '12', 'Seite geändert', 811);
self::assertSame(4711, $id);
}
public function testRecordReturnsNullWhenTheInsertFails(): void
{
Functions\when('current_time')->justReturn('2026-09-25 08:00:00');
Functions\when('get_current_user_id')->justReturn(3);
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->allows('insert')->andReturn(false);
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
$id = (new ChangeLog($db))->record('lit/content-update', 'page', '12', 'x');
} finally {
ini_set('error_log', (string) $previous);
}
self::assertNull($id);
self::assertStringContainsString('change log insert failed', (string) file_get_contents($log));
unlink($log);
}
public function testRecordTruncatesObjectIdAndAbilityToTheColumnSizes(): void
{
Functions\when('current_time')->justReturn('2026-09-25 08:00:00');
Functions\when('get_current_user_id')->justReturn(3);
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->expects('insert')
->with('wp_lit_mcp_log', Mockery::on(static function (array $row): bool {
return $row['object_id'] === str_repeat('ä', 191) && $row['ability'] === str_repeat('ö', 100);
}), Mockery::type('array'))
->andReturn(1);
(new ChangeLog($db))->record(str_repeat('ö', 150), 'option', str_repeat('ä', 300), 'x');
}
public function testRecordPartialStoresResultPartial(): void
{
Functions\when('current_time')->justReturn('2026-09-25 08:00:00');
Functions\when('get_current_user_id')->justReturn(3);
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->expects('insert')
->with('wp_lit_mcp_log', Mockery::on(static function (array $row): bool {
return $row['result'] === 'partial' && $row['revision_id'] === 7 && $row['object_id'] === '12';
}), Mockery::type('array'))
->andReturn(1);
(new ChangeLog($db))->recordPartial('lit/content-update', 'page', '12', 'teilweise', 7);
}
public function testRecordPartialNeverThrows(): void
{
Functions\when('current_time')->justReturn('2026-09-25 08:00:00');
Functions\when('get_current_user_id')->justReturn(3);
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->allows('insert')->andThrow(new \RuntimeException('db gone'));
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
(new ChangeLog($db))->recordPartial('lit/menu-set-items', 'nav_menu', '4', 'teilweise');
} finally {
ini_set('error_log', (string) $previous);
}
self::assertStringContainsString('db gone', (string) file_get_contents($log));
unlink($log);
}
public function testPurgeDeletesRowsOlderThanTheRetention(): void
{
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->expects('query')
->with(Mockery::on(static fn (string $sql): bool => str_starts_with(
$sql,
"DELETE FROM wp_lit_mcp_log WHERE created_at < '"
)))
->andReturn(3);
self::assertSame(3, (new ChangeLog($db))->purgeOlderThan(90));
}
public function testQueryFiltersAndPaginatesNewestFirst(): void
{
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->expects('get_var')
->with(Mockery::on(static fn (string $sql): bool => $sql
=== "SELECT COUNT(*) FROM wp_lit_mcp_log WHERE 1 = 1 AND ability = 'lit/content-update'"
. " AND user_id = 3"))
->andReturn('12');
$db->expects('get_results')
->with(
Mockery::on(static fn (string $sql): bool => str_ends_with($sql, 'ORDER BY id DESC LIMIT 50 OFFSET 50')),
'ARRAY_A'
)
->andReturn([['id' => '7', 'ability' => 'lit/content-update']]);
$result = (new ChangeLog($db))->query(['ability' => 'lit/content-update', 'user_id' => 3], 50, 50);
self::assertSame(12, $result['total']);
self::assertSame('7', $result['rows'][0]['id']);
}
public function testQueryFiltersByResultAndDateRange(): void
{
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->expects('get_var')
->with(Mockery::on(static fn (string $sql): bool => $sql
=== "SELECT COUNT(*) FROM wp_lit_mcp_log WHERE 1 = 1 AND result = 'partial'"
. " AND created_at >= '2026-09-01 00:00:00' AND created_at <= '2026-09-25 23:59:59'"))
->andReturn('2');
$db->expects('get_results')->andReturn([]);
$result = (new ChangeLog($db))->query(
['result' => 'partial', 'date_from' => '2026-09-01', 'date_to' => '2026-09-25'],
50,
0
);
self::assertSame(2, $result['total']);
self::assertSame([], $result['rows']);
}
public function testQueryWithoutFiltersUsesTheBaseCondition(): void
{
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->expects('get_var')
->with('SELECT COUNT(*) FROM wp_lit_mcp_log WHERE 1 = 1')
->andReturn('0');
$db->expects('get_results')->andReturn(null);
$result = (new ChangeLog($db))->query([], 50, 0);
self::assertSame(0, $result['total']);
self::assertSame([], $result['rows']);
}
public function testAbilitiesAndClear(): void
{
$db = Mockery::mock(\wpdb::class)->makePartial();
$db->expects('get_col')->with('SELECT DISTINCT ability FROM wp_lit_mcp_log ORDER BY ability')->andReturn(['lit/a']);
$db->expects('query')->with('DELETE FROM wp_lit_mcp_log')->andReturn(4);
$log = new ChangeLog($db);
self::assertSame(['lit/a'], $log->abilities());
$log->clear();
}
}