Skip to content

Commit 51a9602

Browse files
Andy ScherzingerAndyScherzinger
authored andcommitted
test(migration): verify AclTimestampBackfill backfill logic and AclMapper timestamp behaviour
Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent 80e3cfb commit 51a9602

4 files changed

Lines changed: 196 additions & 4 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
93102
1+
96706

tests/unit/Db/AclMapperTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,34 @@ public function testFindBoardIdDatabase() {
120120
$this->assertEquals($this->boards[0]->getId(), $this->aclMapper->findBoardId($this->acls[1]->getId()));
121121
}
122122

123+
public function testInsertSetsCreatedAtAndLastModifiedAt(): void {
124+
$before = time();
125+
$acl = $this->getAcl('user', 'timestamps_user', false, false, false, $this->boards[0]->getId());
126+
$inserted = $this->aclMapper->insert($acl);
127+
$after = time();
128+
129+
$this->assertGreaterThanOrEqual($before, $inserted->getCreatedAt());
130+
$this->assertLessThanOrEqual($after, $inserted->getCreatedAt());
131+
$this->assertGreaterThanOrEqual($before, $inserted->getLastModifiedAt());
132+
$this->assertLessThanOrEqual($after, $inserted->getLastModifiedAt());
133+
134+
$this->aclMapper->delete($inserted);
135+
}
136+
137+
public function testUpdateChangesLastModifiedAtButNotCreatedAt(): void {
138+
$acl = $this->getAcl('user', 'timestamps_user2', false, false, false, $this->boards[0]->getId());
139+
$inserted = $this->aclMapper->insert($acl);
140+
$originalCreatedAt = $inserted->getCreatedAt();
141+
142+
$inserted->setPermissionEdit(true);
143+
$updated = $this->aclMapper->update($inserted);
144+
145+
$this->assertSame($originalCreatedAt, $updated->getCreatedAt());
146+
$this->assertGreaterThan(0, $updated->getLastModifiedAt());
147+
148+
$this->aclMapper->delete($updated);
149+
}
150+
123151
public function tearDown(): void {
124152
parent::tearDown();
125153
foreach ($this->acls as $acl) {

tests/unit/Db/AclTest.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ public function testJsonSerialize() {
5959
'permissionEdit' => true,
6060
'permissionShare' => true,
6161
'permissionManage' => true,
62-
'owner' => false
62+
'owner' => false,
63+
'createdAt' => 0,
64+
'lastModifiedAt' => 0,
6365
], $acl->jsonSerialize());
6466
$acl = $this->createAclGroup();
6567
$this->assertEquals([
@@ -70,7 +72,9 @@ public function testJsonSerialize() {
7072
'permissionEdit' => true,
7173
'permissionShare' => true,
7274
'permissionManage' => true,
73-
'owner' => false
75+
'owner' => false,
76+
'createdAt' => 0,
77+
'lastModifiedAt' => 0,
7478
], $acl->jsonSerialize());
7579
}
7680

@@ -85,7 +89,9 @@ public function testSetOwner() {
8589
'permissionEdit' => true,
8690
'permissionShare' => true,
8791
'permissionManage' => true,
88-
'owner' => true
92+
'owner' => true,
93+
'createdAt' => 0,
94+
'lastModifiedAt' => 0,
8995
], $acl->jsonSerialize());
9096
}
9197

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\Deck\Migration;
11+
12+
use OCP\DB\IResult;
13+
use OCP\DB\QueryBuilder\IExpressionBuilder;
14+
use OCP\DB\QueryBuilder\IQueryBuilder;
15+
use OCP\IDBConnection;
16+
use OCP\Migration\IOutput;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use Test\TestCase;
19+
20+
class AclTimestampBackfillTest extends TestCase {
21+
22+
private IDBConnection&MockObject $db;
23+
private IOutput&MockObject $output;
24+
private AclTimestampBackfill $backfill;
25+
26+
protected function setUp(): void {
27+
parent::setUp();
28+
$this->db = $this->createMock(IDBConnection::class);
29+
$this->output = $this->createMock(IOutput::class);
30+
$this->backfill = new AclTimestampBackfill($this->db);
31+
}
32+
33+
public function testGetName(): void {
34+
$this->assertNotEmpty($this->backfill->getName());
35+
}
36+
37+
public function testRunNoRowsIsNoop(): void {
38+
[$selectQb] = $this->buildSelectQb([]);
39+
40+
$this->db->method('getQueryBuilder')->willReturn($selectQb);
41+
$this->output->expects($this->once())
42+
->method('info')
43+
->with($this->stringContains('no rows'));
44+
45+
$this->backfill->run($this->output);
46+
}
47+
48+
public function testRunUpdatesTwoRows(): void {
49+
$rows = [
50+
['acl_id' => 1, 'board_last_modified' => 1000000],
51+
['acl_id' => 2, 'board_last_modified' => 2000000],
52+
];
53+
[$selectQb] = $this->buildSelectQb($rows);
54+
$updateQb = $this->buildUpdateQb(2);
55+
56+
$this->db->expects($this->exactly(2))
57+
->method('getQueryBuilder')
58+
->willReturnOnConsecutiveCalls($selectQb, $updateQb);
59+
60+
$this->output->expects($this->once())
61+
->method('info')
62+
->with($this->stringContains('2'));
63+
64+
$this->backfill->run($this->output);
65+
}
66+
67+
public function testRunUsesBoardTimestampWhenAvailable(): void {
68+
$rows = [['acl_id' => 1, 'board_last_modified' => 1234567]];
69+
[$selectQb] = $this->buildSelectQb($rows);
70+
71+
$capturedTs = null;
72+
$updateQb = $this->buildUpdateQb(1, function (string $name, mixed $value) use (&$capturedTs): void {
73+
if ($name === 'ts') {
74+
$capturedTs = $value;
75+
}
76+
});
77+
78+
$this->db->expects($this->exactly(2))
79+
->method('getQueryBuilder')
80+
->willReturnOnConsecutiveCalls($selectQb, $updateQb);
81+
$this->output->method('info');
82+
83+
$this->backfill->run($this->output);
84+
85+
$this->assertSame(1234567, $capturedTs);
86+
}
87+
88+
public function testRunUsesCurrentTimeWhenBoardTimestampIsZero(): void {
89+
$rows = [['acl_id' => 1, 'board_last_modified' => 0]];
90+
[$selectQb] = $this->buildSelectQb($rows);
91+
92+
$capturedTs = null;
93+
$before = time();
94+
$updateQb = $this->buildUpdateQb(1, function (string $name, mixed $value) use (&$capturedTs): void {
95+
if ($name === 'ts') {
96+
$capturedTs = $value;
97+
}
98+
});
99+
100+
$this->db->expects($this->exactly(2))
101+
->method('getQueryBuilder')
102+
->willReturnOnConsecutiveCalls($selectQb, $updateQb);
103+
$this->output->method('info');
104+
105+
$this->backfill->run($this->output);
106+
$after = time();
107+
108+
$this->assertGreaterThanOrEqual($before, $capturedTs);
109+
$this->assertLessThanOrEqual($after, $capturedTs);
110+
}
111+
112+
/**
113+
* @return array{0: IQueryBuilder&MockObject}
114+
*/
115+
private function buildSelectQb(array $rows): array {
116+
$expr = $this->createMock(IExpressionBuilder::class);
117+
$expr->method('eq')->willReturn('1=1');
118+
119+
$result = $this->createMock(IResult::class);
120+
$result->method('fetchAll')->willReturn($rows);
121+
$result->expects($this->once())->method('closeCursor');
122+
123+
$qb = $this->createMock(IQueryBuilder::class);
124+
$qb->method('select')->willReturnSelf();
125+
$qb->method('from')->willReturnSelf();
126+
$qb->method('join')->willReturnSelf();
127+
$qb->method('where')->willReturnSelf();
128+
$qb->method('createNamedParameter')->willReturn('?');
129+
$qb->method('expr')->willReturn($expr);
130+
$qb->method('executeQuery')->willReturn($result);
131+
132+
return [$qb];
133+
}
134+
135+
private function buildUpdateQb(int $expectedExecutions, ?\Closure $onSetParameter = null): IQueryBuilder&MockObject {
136+
$expr = $this->createMock(IExpressionBuilder::class);
137+
$expr->method('eq')->willReturn('1=1');
138+
139+
$qb = $this->createMock(IQueryBuilder::class);
140+
$qb->method('update')->willReturnSelf();
141+
$qb->method('set')->willReturnSelf();
142+
$qb->method('where')->willReturnSelf();
143+
$qb->method('createParameter')->willReturn('?');
144+
$qb->method('expr')->willReturn($expr);
145+
$qb->expects($this->exactly($expectedExecutions))->method('executeStatement');
146+
147+
if ($onSetParameter !== null) {
148+
$qb->method('setParameter')->willReturnCallback(function (string $name, mixed $value) use ($onSetParameter, $qb) {
149+
$onSetParameter($name, $value);
150+
return $qb;
151+
});
152+
} else {
153+
$qb->method('setParameter')->willReturnSelf();
154+
}
155+
156+
return $qb;
157+
}
158+
}

0 commit comments

Comments
 (0)