From e9213e0bc2298764123cbb8ae309d982ddd9111d Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 11 Jun 2026 14:28:41 +0200 Subject: [PATCH 1/8] feat(sharereview): add listener registering Deck as a share source Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Andy Scherzinger --- lib/ShareReview/ShareReviewListener.php | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/ShareReview/ShareReviewListener.php diff --git a/lib/ShareReview/ShareReviewListener.php b/lib/ShareReview/ShareReviewListener.php new file mode 100644 index 000000000..ee8db8c1e --- /dev/null +++ b/lib/ShareReview/ShareReviewListener.php @@ -0,0 +1,27 @@ + */ +class ShareReviewListener implements IEventListener { + public function __construct() { + } + + public function handle(Event $event): void { + if (!$event instanceof SourceEvent) { + return; + } + $event->registerSource(ShareReviewSource::class); + } +} From 0913702d31efdd89608dd198ad30f5cabc388bb8 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 11 Jun 2026 14:29:21 +0200 Subject: [PATCH 2/8] feat(sharereview): add ShareReviewSource with constructor and getName() Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Andy Scherzinger --- lib/ShareReview/ShareReviewSource.php | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 lib/ShareReview/ShareReviewSource.php diff --git a/lib/ShareReview/ShareReviewSource.php b/lib/ShareReview/ShareReviewSource.php new file mode 100644 index 000000000..d153ef05a --- /dev/null +++ b/lib/ShareReview/ShareReviewSource.php @@ -0,0 +1,39 @@ + Date: Thu, 11 Jun 2026 14:30:05 +0200 Subject: [PATCH 3/8] feat(sharereview): implement getShares() with board name lookup via JOIN Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Andy Scherzinger --- lib/ShareReview/ShareReviewSource.php | 82 ++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/lib/ShareReview/ShareReviewSource.php b/lib/ShareReview/ShareReviewSource.php index d153ef05a..5fd73c3fe 100644 --- a/lib/ShareReview/ShareReviewSource.php +++ b/lib/ShareReview/ShareReviewSource.php @@ -9,8 +9,13 @@ namespace OCA\Deck\ShareReview; +use OCA\Deck\Db\Acl; use OCA\ShareReview\Sources\ISource; +use OCP\Constants; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use OCP\Share\IShare; use Psr\Log\LoggerInterface; class ShareReviewSource implements ISource { @@ -29,11 +34,86 @@ public function getName(): string { return 'Deck'; } + /** + * @return list + */ public function getShares(): array { - return []; + $rawShares = $this->fetchAllShares(); + $appName = $this->getName(); + $formatted = []; + foreach ($rawShares as $share) { + $formatted[] = [ + 'id' => (int)$share['id'], + 'app' => $appName, + 'object' => $this->resolveObjectName($share), + 'initiator' => (string)$share['board_owner'], + 'type' => $this->mapParticipantType((int)$share['type']), + 'recipient' => (string)$share['participant'], + 'permissions' => $this->computePermissions($share), + 'password' => false, + 'time' => '1970-01-01 01:00:00', + 'action' => '', + ]; + } + return $formatted; } public function deleteShare(string $shareId): bool { return false; } + + /** @return list> */ + private function fetchAllShares(): array { + try { + $qb = $this->db->getQueryBuilder(); + $qb->select( + 'a.id', 'a.type', 'a.participant', + 'a.permission_edit', 'a.permission_share', 'a.permission_manage' + ) + ->addSelect($qb->createFunction('b.title AS board_title')) + ->addSelect($qb->createFunction('b.owner AS board_owner')) + ->from(self::ACL_TABLE, 'a') + ->leftJoin('a', self::BOARDS_TABLE, 'b', $qb->expr()->eq('a.board_id', 'b.id')) + ->orderBy('a.id', 'ASC'); + $result = $qb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + return $rows; + } catch (Exception $e) { + $this->logger->error('Deck ShareReview: failed to fetch shares: {message}', ['message' => $e->getMessage()]); + return []; + } + } + + /** @param array $share */ + private function resolveObjectName(array $share): string { + $title = (string)($share['board_title'] ?? ''); + $boardId = (int)$share['id']; + return ($title !== '' ? $title : "Board $boardId") . ' (Board)'; + } + + private function mapParticipantType(int $type): int { + return match($type) { + Acl::PERMISSION_TYPE_USER => IShare::TYPE_USER, + Acl::PERMISSION_TYPE_GROUP => IShare::TYPE_GROUP, + Acl::PERMISSION_TYPE_REMOTE => IShare::TYPE_REMOTE, + Acl::PERMISSION_TYPE_CIRCLE => IShare::TYPE_CIRCLE, + default => IShare::TYPE_USER, + }; + } + + /** @param array $share */ + private function computePermissions(array $share): int { + $permissions = Constants::PERMISSION_READ; + if ($share['permission_edit']) { + $permissions |= Constants::PERMISSION_UPDATE | Constants::PERMISSION_CREATE | Constants::PERMISSION_DELETE; + } + if ($share['permission_share']) { + $permissions |= Constants::PERMISSION_SHARE; + } + if ($share['permission_manage']) { + $permissions |= self::PERMISSION_MANAGE; + } + return $permissions; + } } From 162414a85f75788d96aff907cf08088f10647d9e Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 11 Jun 2026 14:30:28 +0200 Subject: [PATCH 4/8] feat(sharereview): implement deleteShare() via direct SQL with logging Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Andy Scherzinger --- lib/ShareReview/ShareReviewSource.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/ShareReview/ShareReviewSource.php b/lib/ShareReview/ShareReviewSource.php index 5fd73c3fe..c558e0067 100644 --- a/lib/ShareReview/ShareReviewSource.php +++ b/lib/ShareReview/ShareReviewSource.php @@ -59,7 +59,16 @@ public function getShares(): array { } public function deleteShare(string $shareId): bool { - return false; + $this->logger->info('Deck ShareReview: deleting share {id}', ['id' => $shareId]); + try { + $qb = $this->db->getQueryBuilder(); + $qb->delete(self::ACL_TABLE) + ->where($qb->expr()->eq('id', $qb->createNamedParameter((int)$shareId, IQueryBuilder::PARAM_INT))); + return $qb->executeStatement() > 0; + } catch (Exception $e) { + $this->logger->error('Deck ShareReview: failed to delete share {id}: {message}', ['id' => $shareId, 'message' => $e->getMessage()]); + return false; + } } /** @return list> */ From 8620aa3db8a89f39d40e8f7ee9ed39b85a8280a4 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 11 Jun 2026 14:31:04 +0200 Subject: [PATCH 5/8] feat(sharereview): register ShareReview listener on SourceEvent Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Andy Scherzinger --- lib/AppInfo/Application.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 51efc0a48..2d85902f7 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -47,10 +47,12 @@ use OCA\Deck\Search\CardCommentProvider; use OCA\Deck\Search\DeckProvider; use OCA\Deck\Service\PermissionService; +use OCA\Deck\ShareReview\ShareReviewListener; use OCA\Deck\Sharing\DeckShareProvider; use OCA\Deck\Sharing\Listener; use OCA\Deck\Teams\DeckTeamResourceProvider; use OCA\Deck\UserMigration\DeckMigrator; +use OCA\ShareReview\Sources\SourceEvent; use OCA\Text\Event\LoadEditor; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -189,6 +191,8 @@ public function register(IRegistrationContext $context): void { $context->registerTeamResourceProvider(DeckTeamResourceProvider::class); $context->registerUserMigrator(DeckMigrator::class); + + $context->registerEventListener(SourceEvent::class, ShareReviewListener::class); } public function registerCommentsEntity(IEventDispatcher $eventDispatcher): void { From babbc6985964d41d048803c0c4ac5f13eba436fd Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 11 Jun 2026 14:31:55 +0200 Subject: [PATCH 6/8] style(sharereview): apply coding standards and Psalm fixes Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Andy Scherzinger --- tests/stub.phpstub | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/stub.phpstub b/tests/stub.phpstub index 8ca73d9dd..28a862978 100644 --- a/tests/stub.phpstub +++ b/tests/stub.phpstub @@ -725,3 +725,15 @@ namespace OCA\NotifyPush\Queue { namespace OCA\Text\Event { class LoadEditor extends \OCP\EventDispatcher\Event {} } + +namespace OCA\ShareReview\Sources { + class SourceEvent extends \OCP\EventDispatcher\Event { + abstract public function registerSource(string $source): void {} + } + + interface ISource { + public function getName(): string; + public function getShares(): array; + public function deleteShare(string $shareId): bool; + } +} From b12ac74cb6ba4048e1b90d575518c40e7de096bf Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 11 Jun 2026 14:33:26 +0200 Subject: [PATCH 7/8] test(sharereview): add unit tests for ShareReviewSource Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Andy Scherzinger --- tests/bootstrap.php | 4 + .../ShareReview/ShareReviewSourceTest.php | 264 ++++++++++++++++++ tests/unit/ShareReview/Stubs.php | 22 ++ 3 files changed, 290 insertions(+) create mode 100644 tests/unit/ShareReview/ShareReviewSourceTest.php create mode 100644 tests/unit/ShareReview/Stubs.php diff --git a/tests/bootstrap.php b/tests/bootstrap.php index dd0c08c49..6a27ef53c 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -24,3 +24,7 @@ require_once __DIR__ . '/../../../tests/bootstrap.php'; require_once __DIR__ . '/../appinfo/autoload.php'; + +if (!interface_exists('OCA\ShareReview\Sources\ISource')) { + require_once __DIR__ . '/unit/ShareReview/Stubs.php'; +} diff --git a/tests/unit/ShareReview/ShareReviewSourceTest.php b/tests/unit/ShareReview/ShareReviewSourceTest.php new file mode 100644 index 000000000..3332493b8 --- /dev/null +++ b/tests/unit/ShareReview/ShareReviewSourceTest.php @@ -0,0 +1,264 @@ +db = $this->createMock(IDBConnection::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->source = new ShareReviewSource($this->db, $this->logger); + } + + private function makeResult(array $rows): MockObject { + $result = $this->createMock(IResult::class); + $result->method('fetchAll')->willReturn($rows); + $result->method('closeCursor')->willReturn(true); + return $result; + } + + private function makeQb(array $fetchRows = [], int $statementRows = 0): MockObject { + $expr = $this->createMock(IExpressionBuilder::class); + $expr->method('eq')->willReturn('1=1'); + + $qb = $this->createMock(IQueryBuilder::class); + $qb->method('select')->willReturnSelf(); + $qb->method('addSelect')->willReturnSelf(); + $qb->method('from')->willReturnSelf(); + $qb->method('leftJoin')->willReturnSelf(); + $qb->method('where')->willReturnSelf(); + $qb->method('orderBy')->willReturnSelf(); + $qb->method('delete')->willReturnSelf(); + $qb->method('createNamedParameter')->willReturn('?'); + $qb->method('createFunction')->willReturnArgument(0); + $qb->method('expr')->willReturn($expr); + $qb->method('executeQuery')->willReturn($this->makeResult($fetchRows)); + $qb->method('executeStatement')->willReturn($statementRows); + + return $qb; + } + + private function makeThrowingQb(): MockObject { + $expr = $this->createMock(IExpressionBuilder::class); + + $qb = $this->createMock(IQueryBuilder::class); + $qb->method('select')->willReturnSelf(); + $qb->method('addSelect')->willReturnSelf(); + $qb->method('from')->willReturnSelf(); + $qb->method('leftJoin')->willReturnSelf(); + $qb->method('where')->willReturnSelf(); + $qb->method('orderBy')->willReturnSelf(); + $qb->method('delete')->willReturnSelf(); + $qb->method('createNamedParameter')->willReturn('?'); + $qb->method('createFunction')->willReturnArgument(0); + $qb->method('expr')->willReturn($expr); + $qb->method('executeQuery')->willThrowException($this->createMock(Exception::class)); + $qb->method('executeStatement')->willThrowException($this->createMock(Exception::class)); + return $qb; + } + + /** @param array $overrides */ + private function makeShareRow(array $overrides = []): array { + return array_merge([ + 'id' => 1, + 'type' => 0, + 'participant' => 'bob', + 'board_title' => 'My Board', + 'board_owner' => 'alice', + 'permission_edit' => 0, + 'permission_share' => 0, + 'permission_manage' => 0, + ], $overrides); + } + + public function testGetName(): void { + $this->assertSame('Deck', $this->source->getName()); + } + + public function testGetSharesEmpty(): void { + $this->db->method('getQueryBuilder')->willReturn($this->makeQb()); + + $this->assertSame([], $this->source->getShares()); + } + + public function testGetSharesUserShare(): void { + $this->db->method('getQueryBuilder')->willReturn($this->makeQb([$this->makeShareRow()])); + + $shares = $this->source->getShares(); + + $this->assertCount(1, $shares); + $share = $shares[0]; + $this->assertSame(1, $share['id']); + $this->assertSame('Deck', $share['app']); + $this->assertSame('My Board (Board)', $share['object']); + $this->assertSame('alice', $share['initiator']); + $this->assertSame(IShare::TYPE_USER, $share['type']); + $this->assertSame('bob', $share['recipient']); + $this->assertSame(Constants::PERMISSION_READ, $share['permissions']); + $this->assertFalse($share['password']); + $this->assertSame('1970-01-01 01:00:00', $share['time']); + $this->assertSame('', $share['action']); + } + + public function testGetSharesGroupShare(): void { + $this->db->method('getQueryBuilder')->willReturn( + $this->makeQb([$this->makeShareRow(['type' => 1, 'participant' => 'developers'])]) + ); + + $shares = $this->source->getShares(); + + $this->assertCount(1, $shares); + $this->assertSame(IShare::TYPE_GROUP, $shares[0]['type']); + $this->assertSame('developers', $shares[0]['recipient']); + } + + public function testGetSharesCircleShare(): void { + $this->db->method('getQueryBuilder')->willReturn( + $this->makeQb([$this->makeShareRow(['type' => 7, 'participant' => 'circle-uid'])]) + ); + + $shares = $this->source->getShares(); + + $this->assertSame(IShare::TYPE_CIRCLE, $shares[0]['type']); + } + + public function testGetSharesRemoteShare(): void { + $this->db->method('getQueryBuilder')->willReturn( + $this->makeQb([$this->makeShareRow(['type' => 6, 'participant' => 'user@remote.example'])]) + ); + + $shares = $this->source->getShares(); + + $this->assertSame(IShare::TYPE_REMOTE, $shares[0]['type']); + } + + public function testGetSharesMissingBoardFallback(): void { + $this->db->method('getQueryBuilder')->willReturn( + $this->makeQb([$this->makeShareRow(['id' => 42, 'board_title' => null, 'board_owner' => null])]) + ); + + $shares = $this->source->getShares(); + + $this->assertCount(1, $shares); + $this->assertSame('Board 42 (Board)', $shares[0]['object']); + } + + public function testGetSharesReturnsEmptyOnDbException(): void { + $this->db->method('getQueryBuilder')->willReturn($this->makeThrowingQb()); + $this->logger->expects($this->once())->method('error'); + + $this->assertSame([], $this->source->getShares()); + } + + public function testComputePermissionsAllFalse(): void { + $this->db->method('getQueryBuilder')->willReturn( + $this->makeQb([$this->makeShareRow([ + 'permission_edit' => 0, + 'permission_share' => 0, + 'permission_manage' => 0, + ])]) + ); + + $shares = $this->source->getShares(); + + $this->assertSame(Constants::PERMISSION_READ, $shares[0]['permissions']); + } + + public function testComputePermissionsEditFlag(): void { + $this->db->method('getQueryBuilder')->willReturn( + $this->makeQb([$this->makeShareRow(['permission_edit' => 1])]) + ); + + $shares = $this->source->getShares(); + + $expected = Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_CREATE | Constants::PERMISSION_DELETE; + $this->assertSame($expected, $shares[0]['permissions']); + } + + public function testComputePermissionsShareFlag(): void { + $this->db->method('getQueryBuilder')->willReturn( + $this->makeQb([$this->makeShareRow(['permission_share' => 1])]) + ); + + $shares = $this->source->getShares(); + + $expected = Constants::PERMISSION_READ | Constants::PERMISSION_SHARE; + $this->assertSame($expected, $shares[0]['permissions']); + } + + public function testComputePermissionsManageFlag(): void { + $this->db->method('getQueryBuilder')->willReturn( + $this->makeQb([$this->makeShareRow(['permission_manage' => 1])]) + ); + + $shares = $this->source->getShares(); + + $expected = Constants::PERMISSION_READ | 32; + $this->assertSame($expected, $shares[0]['permissions']); + } + + public function testComputePermissionsAllTrue(): void { + $this->db->method('getQueryBuilder')->willReturn( + $this->makeQb([$this->makeShareRow([ + 'permission_edit' => 1, + 'permission_share' => 1, + 'permission_manage' => 1, + ])]) + ); + + $shares = $this->source->getShares(); + + $expected = Constants::PERMISSION_READ + | Constants::PERMISSION_UPDATE + | Constants::PERMISSION_CREATE + | Constants::PERMISSION_DELETE + | Constants::PERMISSION_SHARE + | 32; + $this->assertSame($expected, $shares[0]['permissions']); + } + + public function testDeleteShareSuccess(): void { + $this->db->method('getQueryBuilder')->willReturn($this->makeQb([], 1)); + $this->logger->expects($this->once())->method('info'); + + $this->assertTrue($this->source->deleteShare('7')); + } + + public function testDeleteShareNotFound(): void { + $this->db->method('getQueryBuilder')->willReturn($this->makeQb([], 0)); + $this->logger->expects($this->once())->method('info'); + + $this->assertFalse($this->source->deleteShare('99')); + } + + public function testDeleteShareReturnsFalseOnDbException(): void { + $this->db->method('getQueryBuilder')->willReturn($this->makeThrowingQb()); + $this->logger->expects($this->once())->method('info'); + $this->logger->expects($this->once())->method('error'); + + $this->assertFalse($this->source->deleteShare('7')); + } +} diff --git a/tests/unit/ShareReview/Stubs.php b/tests/unit/ShareReview/Stubs.php new file mode 100644 index 000000000..80ec8a517 --- /dev/null +++ b/tests/unit/ShareReview/Stubs.php @@ -0,0 +1,22 @@ + Date: Thu, 11 Jun 2026 14:36:41 +0200 Subject: [PATCH 8/8] fix(sharereview): harden and optimize implementation and testing Assisted-by: Claude Code:claude-sonnet-4-6 Signed-off-by: Andy Scherzinger --- lib/ShareReview/ShareReviewSource.php | 8 ++++---- tests/unit/ShareReview/ShareReviewSourceTest.php | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/ShareReview/ShareReviewSource.php b/lib/ShareReview/ShareReviewSource.php index c558e0067..dddf26d8d 100644 --- a/lib/ShareReview/ShareReviewSource.php +++ b/lib/ShareReview/ShareReviewSource.php @@ -76,11 +76,11 @@ private function fetchAllShares(): array { try { $qb = $this->db->getQueryBuilder(); $qb->select( - 'a.id', 'a.type', 'a.participant', + 'a.id', 'a.board_id', 'a.type', 'a.participant', 'a.permission_edit', 'a.permission_share', 'a.permission_manage' ) - ->addSelect($qb->createFunction('b.title AS board_title')) - ->addSelect($qb->createFunction('b.owner AS board_owner')) + ->selectAlias('b.title', 'board_title') + ->selectAlias('b.owner', 'board_owner') ->from(self::ACL_TABLE, 'a') ->leftJoin('a', self::BOARDS_TABLE, 'b', $qb->expr()->eq('a.board_id', 'b.id')) ->orderBy('a.id', 'ASC'); @@ -97,7 +97,7 @@ private function fetchAllShares(): array { /** @param array $share */ private function resolveObjectName(array $share): string { $title = (string)($share['board_title'] ?? ''); - $boardId = (int)$share['id']; + $boardId = (int)($share['board_id'] ?? $share['id']); return ($title !== '' ? $title : "Board $boardId") . ' (Board)'; } diff --git a/tests/unit/ShareReview/ShareReviewSourceTest.php b/tests/unit/ShareReview/ShareReviewSourceTest.php index 3332493b8..683e77df7 100644 --- a/tests/unit/ShareReview/ShareReviewSourceTest.php +++ b/tests/unit/ShareReview/ShareReviewSourceTest.php @@ -47,13 +47,13 @@ private function makeQb(array $fetchRows = [], int $statementRows = 0): MockObje $qb = $this->createMock(IQueryBuilder::class); $qb->method('select')->willReturnSelf(); $qb->method('addSelect')->willReturnSelf(); + $qb->method('selectAlias')->willReturnSelf(); $qb->method('from')->willReturnSelf(); $qb->method('leftJoin')->willReturnSelf(); $qb->method('where')->willReturnSelf(); $qb->method('orderBy')->willReturnSelf(); $qb->method('delete')->willReturnSelf(); $qb->method('createNamedParameter')->willReturn('?'); - $qb->method('createFunction')->willReturnArgument(0); $qb->method('expr')->willReturn($expr); $qb->method('executeQuery')->willReturn($this->makeResult($fetchRows)); $qb->method('executeStatement')->willReturn($statementRows); @@ -67,13 +67,13 @@ private function makeThrowingQb(): MockObject { $qb = $this->createMock(IQueryBuilder::class); $qb->method('select')->willReturnSelf(); $qb->method('addSelect')->willReturnSelf(); + $qb->method('selectAlias')->willReturnSelf(); $qb->method('from')->willReturnSelf(); $qb->method('leftJoin')->willReturnSelf(); $qb->method('where')->willReturnSelf(); $qb->method('orderBy')->willReturnSelf(); $qb->method('delete')->willReturnSelf(); $qb->method('createNamedParameter')->willReturn('?'); - $qb->method('createFunction')->willReturnArgument(0); $qb->method('expr')->willReturn($expr); $qb->method('executeQuery')->willThrowException($this->createMock(Exception::class)); $qb->method('executeStatement')->willThrowException($this->createMock(Exception::class)); @@ -84,6 +84,7 @@ private function makeThrowingQb(): MockObject { private function makeShareRow(array $overrides = []): array { return array_merge([ 'id' => 1, + 'board_id' => 10, 'type' => 0, 'participant' => 'bob', 'board_title' => 'My Board', @@ -157,7 +158,7 @@ public function testGetSharesRemoteShare(): void { public function testGetSharesMissingBoardFallback(): void { $this->db->method('getQueryBuilder')->willReturn( - $this->makeQb([$this->makeShareRow(['id' => 42, 'board_title' => null, 'board_owner' => null])]) + $this->makeQb([$this->makeShareRow(['board_id' => 42, 'board_title' => null, 'board_owner' => null])]) ); $shares = $this->source->getShares();