retriever->setClient(new Client(['handler' => $handler])); return $mock; } protected function setUp(): void { $this->retriever = new ObjectRetriever; } public function testGetRootResource() { $this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], '{"@context":{"cloudobjects":"coid:\/\/cloudobjects.io\/","rdf":"http:\/\/www.w3.org\/1999\/02\/22-rdf-syntax-ns#","rdfs":"http:\/\/www.w3.org\/2000\/01\/rdf-schema#"},"@id":"coid:\/\/cloudobjects.io","@type":"cloudobjects:Namespace","cloudobjects:hasPublicListing":"true","cloudobjects:revision":"1-325baa62b76105f56dc09386f5a2ec91","rdfs:comment":"The CloudObjects namespace defines the essential objects.","rdfs:label":"CloudObjects"}' )); $coid = new IRI('coid://cloudobjects.io'); // Test node interface first $object = $this->retriever->getObjectNode($coid); $this->assertNotNull($object); $this->assertEquals((string)$coid, $object->getId()); $this->assertNotNull($object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')); $this->assertEquals('CloudObjects', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue()); $this->assertNull($object->getProperty('urn:example:nonexistingvalue')); // Test CloudObject interface $object = $this->retriever->getCloudObject($coid); $this->assertNotNull($object); $this->assertEquals((string)$coid, $object->getAsNode()->getId()); $this->assertEquals($coid, $object->getCOID()); $this->assertNotNull($object->getString('http://www.w3.org/2000/01/rdf-schema#label')); $this->assertEquals('CloudObjects', $object->getString('http://www.w3.org/2000/01/rdf-schema#label')); $this->assertNull($object->getString('urn:example:nonexistingvalue')); } public function testGetNonExistingResource() { $this->setMockResponse(new Response(404)); // should return "null" $coid = new IRI('coid://nonexistingresource.example.com'); $object = $this->retriever->getObjectNode($coid); $this->assertNull($object); } public function testWhenAPIThrowsError() { $this->setMockResponse(new Response(500)); $coid = new IRI('coid://nonexistingresource.example.com'); $this->expectException(CoreAPIException::class); $this->retriever->getObjectNode($coid); } public function testWhenAPITimesOut() { $this->setMockResponse(new ConnectException( 'cURL error 28: Operation timed out', new Request('GET', 'cloudobjects.io') )); $coid = new IRI('coid://cloudobjects.io'); try { $this->retriever->getObjectNode($coid); $this->fail('Expected CoreAPIException was not thrown.'); } catch (CoreAPIException $e) { $this->assertInstanceOf(ConnectException::class, $e->getPrevious()); } } public function testGetAttachment() { $mock = $this->setMockResponse( new Response(200, ['Content-Type' => 'application/ld+json'], '{"@id":"coid://cloudobjects.io/Test","coid://cloudobjects.io/isAtRevision":"1-abc"}'), new Response(200, ['Content-Type' => 'text/plain'], 'Attachment content') ); $content = $this->retriever->getAttachment(new IRI('coid://cloudobjects.io/Test'), 'file.txt'); $this->assertEquals('Attachment content', $content); $this->assertStringEndsWith('cloudobjects.io/Test/file.txt', $mock->getLastRequest()->getUri()->getPath()); } public function testGetMissingAttachment() { $this->setMockResponse( new Response(200, ['Content-Type' => 'application/ld+json'], '{"@id":"coid://cloudobjects.io/Test","coid://cloudobjects.io/isAtRevision":"1-abc"}'), new Response(404) ); $content = $this->retriever->getAttachment(new IRI('coid://cloudobjects.io/Test'), 'missing.txt'); $this->assertNull($content); } private function createFallbackStoreDirectory() : string { $directory = sys_get_temp_dir().DIRECTORY_SEPARATOR.'clobj-fallback-'.uniqid(); mkdir($directory); return $directory; } private function removeDirectory(string $directory) { $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($files as $file) $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); rmdir($directory); } public function testFallbackStoreSavesObjectInScope() { $directory = $this->createFallbackStoreDirectory(); $this->retriever = new ObjectRetriever([ 'fallback_store_directory' => $directory, 'fallback_store_scope' => 'example.com, cloudobjects.io', ]); $body = '{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Test"}'; $this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], $body)); try { $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $location = $directory.'/cloudobjects.io/Test/object.jsonld'; $this->assertFileExists($location); $this->assertEquals($body, file_get_contents($location)); } finally { $this->removeDirectory($directory); } } public function testFallbackStoreSkipsObjectOutOfScope() { $directory = $this->createFallbackStoreDirectory(); $this->retriever = new ObjectRetriever([ 'fallback_store_directory' => $directory, 'fallback_store_scope' => [ 'example.com' ], ]); $this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], '{"@id":"coid://cloudobjects.io/Test"}')); try { $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $this->assertFileDoesNotExist($directory.'/cloudobjects.io/Test/object.jsonld'); } finally { $this->removeDirectory($directory); } } public function testFallbackStoreSavesAllObjectsWithWildcard() { $directory = $this->createFallbackStoreDirectory(); $this->retriever = new ObjectRetriever([ 'fallback_store_directory' => $directory, 'fallback_store_scope' => '*', ]); $this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], '{"@id":"coid://cloudobjects.io"}')); try { $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io')); $this->assertFileExists($directory.'/cloudobjects.io/object.jsonld'); } finally { $this->removeDirectory($directory); } } private function createRetrieverWithStoredObject(string $directory) { $this->retriever = new ObjectRetriever([ 'fallback_store_directory' => $directory, 'fallback_store_scope' => '*', ]); mkdir($directory.'/cloudobjects.io/Test', 0777, true); file_put_contents($directory.'/cloudobjects.io/Test/object.jsonld', '{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Stored"}'); } public function testFallbackStoreUsedWhenAPIReturns500() { $directory = $this->createFallbackStoreDirectory(); $this->createRetrieverWithStoredObject($directory); $this->setMockResponse(new Response(500)); try { $object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $this->assertNotNull($object); $this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue()); } finally { $this->removeDirectory($directory); } } public function testFallbackStoreUsedWhenAPITimesOut() { $directory = $this->createFallbackStoreDirectory(); $this->createRetrieverWithStoredObject($directory); $this->setMockResponse(new ConnectException( 'cURL error 28: Operation timed out', new Request('GET', 'cloudobjects.io') )); try { $object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $this->assertNotNull($object); $this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue()); } finally { $this->removeDirectory($directory); } } public function testFallbackStoreUsedWhenRequestFailsWithoutResponse() { $directory = $this->createFallbackStoreDirectory(); $this->createRetrieverWithStoredObject($directory); $this->setMockResponse(new RequestException( 'Request failed', new Request('GET', 'cloudobjects.io') )); try { $object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $this->assertNotNull($object); $this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue()); } finally { $this->removeDirectory($directory); } } public function testErrorWhenRequestFailsWithoutResponseAndObjectNotInFallbackStore() { $directory = $this->createFallbackStoreDirectory(); $this->retriever = new ObjectRetriever([ 'fallback_store_directory' => $directory, 'fallback_store_scope' => '*', ]); $this->setMockResponse(new RequestException( 'Request failed', new Request('GET', 'cloudobjects.io') )); try { $this->expectException(CoreAPIException::class); $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); } finally { $this->removeDirectory($directory); } } public function testFallbackStoreNotUsedOnClientError() { $directory = $this->createFallbackStoreDirectory(); $this->createRetrieverWithStoredObject($directory); $this->setMockResponse(new Response(403)); try { $this->expectException(CoreAPIException::class); $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); } finally { $this->removeDirectory($directory); } } public function testErrorWhenAPIThrowsErrorAndObjectNotInFallbackStore() { $directory = $this->createFallbackStoreDirectory(); $this->retriever = new ObjectRetriever([ 'fallback_store_directory' => $directory, 'fallback_store_scope' => '*', ]); $this->setMockResponse(new Response(500)); try { $this->expectException(CoreAPIException::class); $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); } finally { $this->removeDirectory($directory); } } private function createRetrieverWithPermanentObject(string $directory, array $options = []) { $this->retriever = new ObjectRetriever(array_merge([ 'permanent_store_directory' => $directory, 'permanent_store_scope' => '*', ], $options)); mkdir($directory.'/cloudobjects.io/Test', 0777, true); file_put_contents($directory.'/cloudobjects.io/Test/object.jsonld', '{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Stored"}'); } public function testPermanentStoreSavesObjectInScope() { $directory = $this->createFallbackStoreDirectory(); $this->retriever = new ObjectRetriever([ 'permanent_store_directory' => $directory, 'permanent_store_scope' => 'example.com, cloudobjects.io', ]); $body = '{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Test"}'; $this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], $body)); try { $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $location = $directory.'/cloudobjects.io/Test/object.jsonld'; $this->assertFileExists($location); $this->assertEquals($body, file_get_contents($location)); } finally { $this->removeDirectory($directory); } } public function testPermanentStoreSkipsObjectOutOfScope() { $directory = $this->createFallbackStoreDirectory(); $this->retriever = new ObjectRetriever([ 'permanent_store_directory' => $directory, 'permanent_store_scope' => [ 'example.com' ], ]); $this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], '{"@id":"coid://cloudobjects.io/Test"}')); try { $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $this->assertFileDoesNotExist($directory.'/cloudobjects.io/Test/object.jsonld'); } finally { $this->removeDirectory($directory); } } public function testPermanentStoreUsedWithoutContactingAPI() { $directory = $this->createFallbackStoreDirectory(); $this->createRetrieverWithPermanentObject($directory); $mock = new MockHandler([ new Response(500) ]); $this->retriever->setClient(new Client(['handler' => HandlerStack::create($mock)])); try { $object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $this->assertNotNull($object); $this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue()); // The mocked response has not been consumed $this->assertCount(1, $mock); } finally { $this->removeDirectory($directory); } } public function testPermanentStoreNotOverwritten() { $directory = $this->createFallbackStoreDirectory(); $this->createRetrieverWithPermanentObject($directory); $this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], '[{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Updated"}]')); try { $this->retriever->fetchAllObjectsInNamespace(new IRI('coid://cloudobjects.io')); $this->assertStringContainsString('Stored', file_get_contents($directory.'/cloudobjects.io/Test/object.jsonld')); } finally { $this->removeDirectory($directory); } } public function testPermanentStoreTakesPrecedenceOverFallbackStore() { $permanentDirectory = $this->createFallbackStoreDirectory(); $fallbackDirectory = $this->createFallbackStoreDirectory(); $this->createRetrieverWithPermanentObject($permanentDirectory, [ 'fallback_store_directory' => $fallbackDirectory, 'fallback_store_scope' => '*', ]); mkdir($fallbackDirectory.'/cloudobjects.io/Test', 0777, true); file_put_contents($fallbackDirectory.'/cloudobjects.io/Test/object.jsonld', '{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Fallback"}'); $this->setMockResponse(new Response(500)); try { $object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $this->assertEquals('Stored', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue()); } finally { $this->removeDirectory($permanentDirectory); $this->removeDirectory($fallbackDirectory); } } private function createStaticObjectDirectory() : string { $directory = $this->createFallbackStoreDirectory(); mkdir($directory.'/cloudobjects.io/Test', 0777, true); file_put_contents($directory.'/cloudobjects.io/Test/object.jsonld', '{"@id":"coid://cloudobjects.io/Test","http://www.w3.org/2000/01/rdf-schema#label":"Static"}'); return $directory; } public function testStaticObjectDirectoryUsedWithoutContactingAPI() { $directory = $this->createStaticObjectDirectory(); $this->retriever = new ObjectRetriever([ 'static_object_directory' => $directory, ]); $mock = new MockHandler([ new Response(500) ]); $this->retriever->setClient(new Client(['handler' => HandlerStack::create($mock)])); try { $object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $this->assertEquals('Static', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue()); // The mocked response has not been consumed $this->assertCount(1, $mock); } finally { $this->removeDirectory($directory); } } public function testStaticObjectDirectoryWithDeprecatedOptionName() { $directory = $this->createStaticObjectDirectory(); $this->retriever = new ObjectRetriever([ 'static_config_path' => $directory, ]); $this->setMockResponse(new Response(500)); try { $object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Test')); $this->assertEquals('Static', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue()); } finally { $this->removeDirectory($directory); } } public function testStaticObjectDirectoryFallsThroughToAPI() { $directory = $this->createStaticObjectDirectory(); $this->retriever = new ObjectRetriever([ 'static_object_directory' => $directory, ]); $this->setMockResponse(new Response(200, ['Content-Type' => 'application/ld+json'], '{"@id":"coid://cloudobjects.io/Other","http://www.w3.org/2000/01/rdf-schema#label":"API"}')); try { $object = $this->retriever->getObjectNode(new IRI('coid://cloudobjects.io/Other')); $this->assertEquals('API', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue()); } finally { $this->removeDirectory($directory); } } }