From 49425c8d761ab7cbdc11560b36383251714f74e8 Mon Sep 17 00:00:00 2001 From: Kaitlin Newson Date: Fri, 19 Jun 2026 09:57:23 -0300 Subject: [PATCH] pkp/pkp-lib#12911 fix ONIX export when no funding plugin is installed and add test --- .../filter/MonographONIX30XmlFilter.php | 12 +- .../tests/MonographONIX30XmlFilterTest.php | 275 ++++++++++++++++++ 2 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 plugins/importexport/onix30/tests/MonographONIX30XmlFilterTest.php diff --git a/plugins/importexport/onix30/filter/MonographONIX30XmlFilter.php b/plugins/importexport/onix30/filter/MonographONIX30XmlFilter.php index 26962aca1e..a3741a0c1b 100644 --- a/plugins/importexport/onix30/filter/MonographONIX30XmlFilter.php +++ b/plugins/importexport/onix30/filter/MonographONIX30XmlFilter.php @@ -3,8 +3,8 @@ /** * @file plugins/importexport/onix30/filter/MonographONIX30XmlFilter.php * - * Copyright (c) 2014-2025 Simon Fraser University - * Copyright (c) 2000-2025 John Willinsky + * Copyright (c) 2014-2026 Simon Fraser University + * Copyright (c) 2000-2026 John Willinsky * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class MonographONIX30XmlFilter @@ -591,7 +591,7 @@ public function createProductNode(DOMDocument $doc, Submission $submission, Publ /* --- Funders and awards --- */ $fundingData = $this->getFundingData($context->getId(), $publication->getData('submissionId')); - if (!$fundingData->isEmpty()) { + if ($fundingData?->isNotEmpty()) { foreach ($fundingData as $funder) { $publisherNode = $doc->createElementNS($deployment->getNamespace(), 'Publisher'); $publisherNode->appendChild($this->buildTextNode($doc, 'PublishingRole', '16')); // 16 -> Funding body @@ -992,10 +992,10 @@ public function buildTextNode(DOMDocument $doc, string $nodeName, string $textCo /** * Helper function to retrieve funding data when available. */ - public function getFundingData(int $contextId, int $submissionId): Collection|false + public function getFundingData(int $contextId, int $submissionId): ?Collection { if (!PluginRegistry::getPlugin('generic', 'FundingPlugin')) { - return false; + return null; } $fundingData = DB::table('funders AS f') @@ -1017,6 +1017,6 @@ public function getFundingData(int $contextId, int $submissionId): Collection|fa ->get() ->groupBy('funder_id'); - return $fundingData ?? false; + return $fundingData ?? null; } } diff --git a/plugins/importexport/onix30/tests/MonographONIX30XmlFilterTest.php b/plugins/importexport/onix30/tests/MonographONIX30XmlFilterTest.php new file mode 100644 index 0000000000..449fff57e3 --- /dev/null +++ b/plugins/importexport/onix30/tests/MonographONIX30XmlFilterTest.php @@ -0,0 +1,275 @@ +registerMockDaos(); + $this->registerMockRequest(); + + $filter = $this->createFilter(); + $submission = $this->createMonograph(); + + $doc = $filter->process($submission); + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('onix', self::ONIX_NS); + + // Root message + $root = $doc->documentElement; + self::assertSame('ONIXMessage', $root->localName); + self::assertSame('3.0', $root->getAttribute('release')); + + // Header / Sender + self::assertSame('Test Press', $this->xpathString($xpath, '//onix:Header/onix:Sender/onix:SenderName')); + + // Exactly one Product for the single publication format + self::assertSame(1, $xpath->query('//onix:Product')->length); + self::assertStringContainsString( + '.testpress.' . self::FORMAT_ID, + $this->xpathString($xpath, '//onix:Product/onix:RecordReference') + ); + self::assertSame('03', $this->xpathString($xpath, '//onix:Product/onix:NotificationType')); + + // ISBN product identifier + self::assertSame( + self::TEST_ISBN, + $this->xpathString($xpath, "//onix:ProductIdentifier[onix:ProductIDType='15']/onix:IDValue") + ); + + // Title + self::assertSame( + 'A Basic Monograph', + $this->xpathString($xpath, '//onix:TitleElement/onix:TitleWithoutPrefix') + ); + + // Keywords + self::assertSame( + 'History, Science', + $this->xpathString($xpath, "//onix:Subject[onix:SubjectSchemeIdentifier='20']/onix:SubjectHeadingText") + ); + + // No authors -> NoContributor element is emitted + self::assertSame(1, $xpath->query('//onix:DescriptiveDetail/onix:NoContributor')->length); + + // Publisher is present + self::assertSame(1, $xpath->query("//onix:Publisher[onix:PublishingRole='01']")->length); + } + + // + // Fixture helpers + // + + /** + * Construct the filter with a deployment and press. + */ + private function createFilter(): MonographONIX30XmlFilter + { + $filterGroup = new FilterGroup(); + $filterGroup->setInputType('primitive::string'); + $filterGroup->setOutputType('primitive::string'); + + $filter = new MonographONIX30XmlFilter($filterGroup); + $filter->setDeployment(new Onix30ExportDeployment($this->createPress(), null)); + + return $filter; + } + + /** + * Create a minimal press. + */ + private function createPress(): Press + { + $press = new Press(); + $press->setId(1); + $press->setData('primaryLocale', 'en'); + $press->setData('name', ['en' => 'Test Press']); + $press->setData('urlPath', 'testpress'); + $press->setData('contactName', 'Press Contact'); + $press->setData('contactEmail', 'press@example.org'); + $press->setData('publisher', 'Test Publisher'); + $press->setData('codeType', '01'); + $press->setData('codeValue', 'TEST'); + return $press; + } + + /** + * Create a minimal monograph. + */ + private function createMonograph(): Submission + { + $publicationFormat = $this->createPublicationFormat(); + + /** @var Publication&MockObject $publication */ + $publication = $this->getMockBuilder(Publication::class) + ->onlyMethods(['getCoverImageUrl']) + ->getMock(); + $publication->method('getCoverImageUrl')->willReturn(''); + $publication->setData('submissionId', 9); + $publication->setData('locale', 'en'); + $publication->setData('title', 'A Basic Monograph', 'en'); + $publication->setData('abstract', 'A short abstract.', 'en'); + $publication->setData('keywords', ['en' => ['History', 'Science']]); + $publication->setData('authors', collect([])); + $publication->setData('publicationFormats', collect([$publicationFormat])); + + /** @var Submission&MockObject $submission */ + $submission = $this->getMockBuilder(Submission::class) + ->onlyMethods(['getCurrentPublication']) + ->getMock(); + $submission->method('getCurrentPublication')->willReturn($publication); + + return $submission; + } + + /** + * A physical publication format with a single ISBN identification code and no + * markets/sales rights/publishing dates. + */ + private function createPublicationFormat(): PublicationFormat + { + $isbn = new IdentificationCode(); + $isbn->setCode('15'); // ISBN-13 + $isbn->setValue(self::TEST_ISBN); + + /** @var PublicationFormat&MockObject $format */ + $format = $this->getMockBuilder(PublicationFormat::class) + ->onlyMethods([ + 'getId', + 'getPhysicalFormat', + 'getIdentificationCodes', + 'getMarkets', + 'getSalesRights', + 'getPublicationDates', + ]) + ->getMock(); + $format->method('getId')->willReturn(self::FORMAT_ID); + $format->method('getPhysicalFormat')->willReturn(true); + $format->method('getIdentificationCodes')->willReturn($this->fakeIterator([$isbn])); + $format->method('getMarkets')->willReturn($this->fakeIterator([])); + $format->method('getSalesRights')->willReturn($this->fakeIterator([])); + $format->method('getPublicationDates')->willReturn($this->fakeIterator([])); + $format->setData('entryKey', 'BC'); // ProductForm: paperback + return $format; + } + + /** + * Register a stub codelist DAO. + */ + private function registerMockDaos(): void + { + /** @var ONIXCodelistItemDAO&MockObject $codelistDao */ + $codelistDao = $this->getMockBuilder(ONIXCodelistItemDAO::class) + ->disableOriginalConstructor() + ->onlyMethods(['codeExistsInList']) + ->getMock(); + $codelistDao->method('codeExistsInList')->willReturn(false); + DAORegistry::registerDAO('ONIXCodelistItemDAO', $codelistDao); + } + + /** + * Register a request mock providing the accessors the filter needs. + */ + private function registerMockRequest(): void + { + /** @var Dispatcher&MockObject $dispatcher */ + $dispatcher = $this->getMockBuilder(Dispatcher::class) + ->onlyMethods(['url']) + ->getMock(); + $dispatcher->method('url')->willReturn('https://example.org/testpress'); + + /** @var Request&MockObject $request */ + $request = $this->getMockBuilder(Request::class) + ->onlyMethods(['getServerHost', 'getDispatcher', 'url']) + ->getMock(); + $request->method('getServerHost')->willReturn('example.org'); + $request->method('getDispatcher')->willReturn($dispatcher); + $request->method('url')->willReturn('https://example.org/testpress/catalog/book/9'); + + Registry::set('request', $request); + } + + /** + * Minimal stand-in for a DAOResultFactory. + */ + private function fakeIterator(array $items): object + { + return new class ($items) { + private array $items; + + public function __construct(array $items) + { + $this->items = array_values($items); + } + + public function next() + { + return array_shift($this->items) ?? false; + } + }; + } + + private function xpathString(DOMXPath $xpath, string $query): string + { + $node = $xpath->query($query)->item(0); + self::assertNotNull($node, "Expected a node for XPath: {$query}"); + return $node->textContent; + } +}