Skip to content

Commit a7fb36f

Browse files
nilmergclaude
andcommitted
RotationRepositoryTest: Cover the new move() method
Verify that moving a rotation frees up its current priority, shifts the in-between siblings in the right direction and finally places it at the new priority. Also handles moving to the same priority. The shared statement helper now defaults to `PDO::FETCH_OBJ` (as the module's connection does), which `move()` relies on; the ORM paths override it to `PDO::FETCH_ASSOC` themselves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent de24f3c commit a7fb36f

1 file changed

Lines changed: 190 additions & 2 deletions

File tree

test/php/library/Notifications/Repository/RotationRepositoryTest.php

Lines changed: 190 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ class RotationRepositoryTest extends TestCase
4141
/**
4242
* Build a real PDOStatement yielding the given rows
4343
*
44-
* `Connection::select()` is declared to return a {@see PDOStatement} and the ORM both calls `setFetchMode()` on
45-
* and iterates the result. A genuine SQLite-backed statement satisfies all of that without a production database.
44+
* `Connection::select()` is declared to return a {@see PDOStatement} and the result is both iterated and, in the
45+
* ORM's case, reconfigured via `setFetchMode()`. A genuine SQLite-backed statement satisfies all of that without a
46+
* production database. The default fetch mode mirrors the module's connection (`PDO::FETCH_OBJ`, see
47+
* `Common\Database`), which is what `move()` relies on; the ORM overrides it to `PDO::FETCH_ASSOC` itself.
4648
*
4749
* @param list<array<string, mixed>> $rows All rows must share the same keys, which become the result's columns
4850
*
@@ -53,6 +55,7 @@ private function selectResult(array $rows): PDOStatement
5355
$columns = empty($rows) ? ['id'] : array_keys($rows[0]);
5456

5557
$pdo = new PDO('sqlite::memory:');
58+
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
5659
$pdo->exec('CREATE TABLE result (' . implode(', ', array_map(fn ($c) => '"' . $c . '"', $columns)) . ')');
5760

5861
if (! empty($rows)) {
@@ -449,4 +452,189 @@ public function testDeleteSoftDeletesVersionAndShiftsSiblings(): void
449452

450453
$this->assertTrue($siblingShifted, 'The higher-priority sibling was not shifted down');
451454
}
455+
456+
/**
457+
* Run a directional move and verify the issued statements
458+
*
459+
* A move always frees up the rotation's current priority first, then shifts the siblings between the old and the
460+
* new priority (queried via a plain {@see Select}), and finally places the rotation at its new priority.
461+
*
462+
* @param int $currentPriority The rotation's current priority
463+
* @param int $newPriority The priority to move the rotation to
464+
* @param array<string, int> $expectedSelectWhere The conditions the sibling query is expected to carry
465+
* @param string $expectedOrder The order the siblings are expected to be queried in
466+
* @param string $expectedExpression The expression the siblings' priority is expected to be adjusted by
467+
* @param list<int> $siblingIds The ids of the siblings to be shifted, in the order they're queried
468+
*
469+
* @return void
470+
*/
471+
private function assertDirectionalMove(
472+
int $currentPriority,
473+
int $newPriority,
474+
array $expectedSelectWhere,
475+
string $expectedOrder,
476+
string $expectedExpression,
477+
array $siblingIds
478+
): void {
479+
$start = (int) (new DateTime())->format('Uv');
480+
481+
$rotation = (new Rotation())->setProperties([
482+
'id' => 42,
483+
'schedule_id' => 1,
484+
'priority' => $currentPriority
485+
]);
486+
487+
$databaseMock = $this->createMock(Connection::class);
488+
$databaseMock->method('quoteIdentifier')
489+
->willReturnArgument(0);
490+
491+
$databaseMock->expects($this->never())
492+
->method('insert');
493+
494+
$databaseMock->expects($this->once())
495+
->method('select')
496+
->with($this->callback(function (Select $select) use ($expectedSelectWhere, $expectedOrder) {
497+
$this->assertSame(['id'], $select->getColumns());
498+
$this->assertSame(['rotation'], $select->getFrom());
499+
$this->assertSame([[$expectedOrder, null]], $select->getOrderBy());
500+
$this->assertSame(['AND', [['AND', $expectedSelectWhere]]], $select->getWhere());
501+
502+
return true;
503+
}))
504+
->willReturn($this->selectResult(array_map(fn ($id) => ['id' => $id], $siblingIds)));
505+
506+
$sequence = [];
507+
$shifted = [];
508+
$databaseMock->expects($this->exactly(count($siblingIds) + 2))
509+
->method('update')
510+
->willReturnCallback(
511+
function (
512+
$table,
513+
$data,
514+
$where
515+
) use (
516+
$start,
517+
$newPriority,
518+
$expectedExpression,
519+
&$sequence,
520+
&$shifted
521+
) {
522+
$this->assertSame('rotation', $table);
523+
524+
if ($where === ['id = ?' => 42] && ($data['deleted'] ?? null) === 'y') {
525+
// Free up the rotation's current priority (note: this one carries no changed_at)
526+
$this->assertSame(['priority' => null, 'deleted' => 'y'], $data);
527+
$sequence[] = 'free';
528+
} elseif ($where === ['id = ?' => 42]) {
529+
// Place the rotation at its new priority
530+
$this->assertArrayHasKey('changed_at', $data);
531+
$this->assertGreaterThanOrEqual($start, $data['changed_at']);
532+
unset($data['changed_at']);
533+
$this->assertSame(['priority' => $newPriority, 'deleted' => 'n'], $data);
534+
$sequence[] = 'place';
535+
} else {
536+
// Shift a sibling out of the way
537+
$this->assertInstanceOf(Expression::class, $data['priority']);
538+
$this->assertSame($expectedExpression, $data['priority']->getStatement());
539+
$this->assertArrayHasKey('changed_at', $data);
540+
$this->assertGreaterThanOrEqual($start, $data['changed_at']);
541+
$shifted[] = (int) $where['id = ?'];
542+
$sequence[] = 'shift';
543+
}
544+
545+
return $this->createStub(PDOStatement::class);
546+
}
547+
);
548+
549+
(new RotationRepository($databaseMock))->move($rotation, $newPriority);
550+
551+
$this->assertSame('free', $sequence[0] ?? null, 'The current priority must be freed up first');
552+
$this->assertSame('place', end($sequence), 'The rotation must be placed at its new priority last');
553+
$this->assertSame($siblingIds, $shifted, 'The expected siblings were not shifted in the queried order');
554+
}
555+
556+
/**
557+
* Covers moving a rotation to a higher priority (a lower number): the siblings in between are shifted down.
558+
*
559+
* @return void
560+
*/
561+
public function testMoveToHigherPriorityShiftsInBetweenSiblingsDown(): void
562+
{
563+
$this->assertDirectionalMove(
564+
3,
565+
1,
566+
['schedule_id = ?' => 1, 'priority >= ?' => 1, 'priority < ?' => 3],
567+
'priority DESC',
568+
'priority + 1',
569+
[10, 11]
570+
);
571+
}
572+
573+
/**
574+
* Covers moving a rotation to a lower priority (a higher number): the siblings in between are shifted up.
575+
*
576+
* @return void
577+
*/
578+
public function testMoveToLowerPriorityShiftsInBetweenSiblingsUp(): void
579+
{
580+
$this->assertDirectionalMove(
581+
1,
582+
3,
583+
['schedule_id = ?' => 1, 'priority > ?' => 1, 'priority <= ?' => 3],
584+
'priority ASC',
585+
'priority - 1',
586+
[20, 21]
587+
);
588+
}
589+
590+
/**
591+
* Covers moving a rotation to its current priority: no siblings are queried or shifted, the rotation is merely
592+
* freed up and placed again.
593+
*
594+
* @return void
595+
*/
596+
public function testMoveToSamePriorityDoesNotShiftSiblings(): void
597+
{
598+
$start = (int) (new DateTime())->format('Uv');
599+
600+
$rotation = (new Rotation())->setProperties([
601+
'id' => 42,
602+
'schedule_id' => 1,
603+
'priority' => 2
604+
]);
605+
606+
$databaseMock = $this->createMock(Connection::class);
607+
$databaseMock->method('quoteIdentifier')
608+
->willReturnArgument(0);
609+
610+
$databaseMock->expects($this->never())
611+
->method('select');
612+
$databaseMock->expects($this->never())
613+
->method('insert');
614+
615+
$sequence = [];
616+
$databaseMock->expects($this->exactly(2))
617+
->method('update')
618+
->willReturnCallback(function ($table, $data, $where) use ($start, &$sequence) {
619+
$this->assertSame('rotation', $table);
620+
$this->assertSame(['id = ?' => 42], $where);
621+
622+
if (($data['deleted'] ?? null) === 'y') {
623+
$this->assertSame(['priority' => null, 'deleted' => 'y'], $data);
624+
$sequence[] = 'free';
625+
} else {
626+
$this->assertArrayHasKey('changed_at', $data);
627+
$this->assertGreaterThanOrEqual($start, $data['changed_at']);
628+
unset($data['changed_at']);
629+
$this->assertSame(['priority' => 2, 'deleted' => 'n'], $data);
630+
$sequence[] = 'place';
631+
}
632+
633+
return $this->createStub(PDOStatement::class);
634+
});
635+
636+
(new RotationRepository($databaseMock))->move($rotation, 2);
637+
638+
$this->assertSame(['free', 'place'], $sequence);
639+
}
452640
}

0 commit comments

Comments
 (0)