def setUp(self): super(TestREST_API, self).setUp() self.pid = self.fedora_fixtures_ingested[0] self.opener = AuthorizingServerConnection(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD) self.rest_api = REST_API(self.opener) self.today = datetime.utcnow().date()
def test_retries(self): with patch('eulfedora.api.requests.adapters') as mockreq_adapters: # retries not specified, retries = None REST_API(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD) # no custom adapter code needed mockreq_adapters.HTTPAdapter.assert_not_called() # retry value specified REST_API(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD, retries=3) # adapter should be initialized with max retries option mockreq_adapters.HTTPAdapter.assert_called_with(max_retries=3)
def setUp(self): super(TestREST_API, self).setUp() self.pid = self.fedora_fixtures_ingested[0] self.rest_api = REST_API(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD) self.today = datetime.utcnow().date()
class TestREST_API(FedoraTestCase): fixtures = ['object-with-pid.foxml'] pidspace = FEDORA_PIDSPACE TEXT_CONTENT = """This is my text content for a new datastream. Hey, nonny-nonny.""" def _add_text_datastream(self): # add a text datastream to the current test object - used by multiple tests FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write(self.TEXT_CONTENT) FILE.flush() # info for calling addDatastream, and return ds = { 'id': 'TEXT', 'label': 'text datastream', 'mimeType': 'text/plain', 'controlGroup': 'M', 'logMessage': "creating new datastream", 'checksumType': 'MD5' } # return (r.status_code == requests.codes.created, r.content) with open(FILE.name) as data: r = self.rest_api.addDatastream(self.pid, ds['id'], ds['label'], ds['mimeType'], ds['logMessage'], ds['controlGroup'], content=data, checksumType=ds['checksumType']) FILE.close() return ((r.status_code == requests.codes.created, r.content), ds) def setUp(self): super(TestREST_API, self).setUp() self.pid = self.fedora_fixtures_ingested[0] self.rest_api = REST_API(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD) self.today = datetime.utcnow().date() # API-A calls def test_findObjects(self): # search for current test object (restrict to current pidspace to avoid bogus failures) r = self.rest_api.findObjects("ownerId~tester pid~%s:*" % FEDORA_PIDSPACE) found = r.text self.assert_('<result ' in found) self.assert_('<resultList>' in found) self.assert_('<pid>%s</pid>' % self.pid in found) # crazy search that shouldn't match anything r = self.rest_api.findObjects("title~supercalifragilistexpi...") self.assert_('<objectFields>' not in r.text) # search for everything - get enough results to get a session token # - note that current test fedora includes a number of control objects # - using smaller chunk size to ensure pagination r = self.rest_api.findObjects("title~*", chunksize=2) self.assert_('<listSession>' in r.text) self.assert_('<token>' in r.text) # search by terms r = self.rest_api.findObjects(terms="more dat? in it than a *") self.assert_('<pid>%s</pid>' % self.pid in r.text) # NOTE: not testing resumeFind here because it would require parsing the xml # for the session token - tested at the server/Repository level def test_getDatastreamDissemination(self): r = self.rest_api.getDatastreamDissemination(self.pid, "DC") dc = r.text self.assert_( "<dc:title>A partially-prepared test object</dc:title>" in dc) self.assert_("<dc:description>This object has more data" in dc) self.assert_("<dc:identifier>%s</dc:identifier>" % self.pid in dc) # get server datetime param (the hard way) for testing r = self.rest_api.getDatastream(self.pid, "DC") dsprofile_node = etree.fromstring(r.content, base_url=r.url) created_s = dsprofile_node.xpath('string(m:dsCreateDate)', namespaces={'m': FEDORA_MANAGE_NS}) created = fedoratime_to_datetime(created_s) # with date-time param r = self.rest_api.getDatastreamDissemination(self.pid, "DC", asOfDateTime=created + ONE_SEC) postcreate_dc = r.text self.assertEqual(dc, postcreate_dc) # unchanged within its first sec # bogus datastream self.assertRaises(Exception, self.rest_api.getDatastreamDissemination, self.pid, "BOGUS") # bogus pid self.assertRaises(Exception, self.rest_api.getDatastreamDissemination, "bogus:pid", "BOGUS") # return_http_response response = self.rest_api.getDatastreamDissemination(self.pid, 'DC') self.assert_( isinstance(response, requests.Response), 'getDatastreamDissemination should return a response object') # datastream content should still be accessible self.assertEqual(dc, response.text) # NOTE: getDissemination not available in REST API until Fedora 3.3 def test_getDissemination(self): # testing with built-in fedora dissemination r = self.rest_api.getDissemination(self.pid, "fedora-system:3", "viewItemIndex") self.assert_('<title>Object Items HTML Presentation</title>' in r.text) self.assert_(self.pid in r.text) # return_http_response response = self.rest_api.getDissemination(self.pid, "fedora-system:3", "viewItemIndex") self.assert_(isinstance(response, requests.Response), 'getDissemination should return a response object') # datastream content should still be accessible self.assert_(self.pid in force_text(response.content)) def test_getObjectHistory(self): r = self.rest_api.getObjectHistory(self.pid) self.assert_('<fedoraObjectHistory' in r.text) self.assert_('pid="%s"' % self.pid in r.text) self.assert_('<objectChangeDate>%s' % self.today in r.text) # bogus pid self.assertRaises(Exception, self.rest_api.getObjectHistory, "bogus:pid") def test_getObjectProfile(self): r = self.rest_api.getObjectProfile(self.pid) profile = r.text self.assert_('<objectProfile' in profile) self.assert_('pid="%s"' % self.pid in profile) self.assert_( '<objLabel>A partially-prepared test object</objLabel>' in profile) self.assert_('<objOwnerId>tester</objOwnerId>' in profile) self.assert_('<objCreateDate>%s' % self.today in profile) self.assert_('<objLastModDate>%s' % self.today in profile) self.assert_('<objState>A</objState>' in profile) # unchecked: objDissIndexViewURL, objItemIndexViewURL # get server datetime param (the hard way) for testing profile_node = etree.fromstring(r.content, base_url=r.url) created_s = profile_node.xpath('string(a:objCreateDate)', namespaces={'a': FEDORA_ACCESS_NS}) created = fedoratime_to_datetime(created_s) # with time r = self.rest_api.getObjectProfile(self.pid, asOfDateTime=created + ONE_SEC) profile_now = r.text # NOTE: profile content is not exactly the same because it includes a datetime attribute self.assert_('pid="%s"' % self.pid in profile_now) self.assert_('<objLabel>A partially-prepared test object</objLabel>' in profile_now) # bogus pid self.assertRaises(Exception, self.rest_api.getObjectHistory, "bogus:pid") def test_listDatastreams(self): r = self.rest_api.listDatastreams(self.pid) self.assert_('<objectDatastreams' in r.text) self.assert_( '<datastream dsid="DC" label="Dublin Core" mimeType="text/xml"' in r.text) # bogus pid self.assertRaises(Exception, self.rest_api.listDatastreams, "bogus:pid") def test_listMethods(self): r = self.rest_api.listMethods(self.pid) methods = r.text self.assert_('<objectMethods' in methods) self.assert_('pid="%s"' % self.pid in methods) # default fedora methods, should be available on every object self.assert_('<sDef pid="fedora-system:3" ' in methods) self.assert_('<method name="viewObjectProfile"' in methods) self.assert_('<method name="viewItemIndex"' in methods) # methods for a specified sdef # NOTE: this causes a 404 error; fedora bug? possibly does not work with system methods? # methods = self.rest_api.listMethods(self.pid, 'fedora-system:3') self.assertRaises(Exception, self.rest_api.listMethods, "bogus:pid") # API-M calls def test_addDatastream(self): # returns result from addDatastream call and info used for add ((added, msg), ds) = self._add_text_datastream() self.assertTrue(added) # response from addDatastream r = self.rest_api.getObjectXML(self.pid) message = r.content self.assert_(ds['logMessage'] in force_text(message)) r = self.rest_api.listDatastreams(self.pid) self.assert_( '<datastream dsid="%(id)s" label="%(label)s" mimeType="%(mimeType)s" />' % ds in r.text) r = self.rest_api.getDatastream(self.pid, ds['id']) ds_profile = r.text self.assert_('dsID="%s"' % ds['id'] in ds_profile) self.assert_('<dsLabel>%s</dsLabel>' % ds['label'] in ds_profile) self.assert_('<dsVersionID>%s.0</dsVersionID>' % ds['id'] in ds_profile) self.assert_('<dsCreateDate>%s' % self.today in ds_profile) self.assert_('<dsState>A</dsState>' in ds_profile) self.assert_('<dsMIME>%s</dsMIME>' % ds['mimeType'] in ds_profile) self.assert_('<dsControlGroup>%s</dsControlGroup>' % ds['controlGroup'] in ds_profile) self.assert_('<dsVersionable>true</dsVersionable>' in ds_profile) # content returned from fedora should be exactly what we started with r = self.rest_api.getDatastreamDissemination(self.pid, ds['id']) self.assertEqual(self.TEXT_CONTENT, r.text) # invalid checksum self.assertRaises(ChecksumMismatch, self.rest_api.addDatastream, self.pid, "TEXT2", "text datastream", mimeType="text/plain", logMessage="creating TEXT2", content='<some> text content</some>', checksum='totally-bogus-not-even-an-MD5', checksumType='MD5') # invalid checksum without a checksum type - warning, but no checksum mismatch with warnings.catch_warnings(record=True) as w: self.rest_api.addDatastream( self.pid, "TEXT2", "text datastream", mimeType="text/plain", logMessage="creating TEXT2", content='<some> text content</some>', checksum='totally-bogus-not-even-an-MD5', checksumType=None) self.assertEqual( 1, len(w), 'calling addDatastream with checksum but no checksum type should generate a warning' ) self.assert_( 'Fedora will ignore the checksum' in str(w[0].message)) # attempt to add to a non-existent object FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write("bogus") FILE.flush() with open(FILE.name) as textfile: self.assertRaises(RequestFailed, self.rest_api.addDatastream, 'bogus:pid', 'TEXT', 'text datastream', mimeType='text/plain', logMessage='creating new datastream', controlGroup='M', content=textfile) FILE.close() # relationship predicates for testing rel_isMemberOf = "info:fedora/fedora-system:def/relations-external#isMemberOf" rel_owner = "info:fedora/fedora-system:def/relations-external#owner" def test_addRelationship(self): # rel to resource added = self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, force_text(modelns.hasModel), 'info:fedora/pid:123', False) self.assertTrue(added) r = self.rest_api.getDatastreamDissemination(self.pid, 'RELS-EXT') self.assert_('<hasModel' in r.text) self.assert_('rdf:resource="info:fedora/pid:123"' in r.text) # literal added = self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, self.rel_owner, "johndoe", True) self.assertTrue(added) r = self.rest_api.getDatastreamDissemination(self.pid, 'RELS-EXT') self.assert_('<owner' in r.text) self.assert_('>johndoe<' in r.text) # bogus pid self.assertRaises(RequestFailed, self.rest_api.addRelationship, 'bogus:pid', 'info:fedora/bogus:pid', self.rel_owner, 'johndoe', True) def test_getRelationships(self): # add relations to retrieve self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, force_text(modelns.hasModel), "info:fedora/pid:123", False) self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, self.rel_owner, "johndoe", True) r = self.rest_api.getRelationships(self.pid) graph = parse_rdf(r.content, r.url) # check total number: fedora-system cmodel + two just added self.assertEqual(3, len(list(graph))) # newly added triples should be included in the graph self.assert_((URIRef('info:fedora/%s' % self.pid), modelns.hasModel, URIRef('info:fedora/pid:123')) in graph) self.assertEqual( 'johndoe', str( graph.value(subject=URIRef('info:fedora/%s' % self.pid), predicate=URIRef(self.rel_owner)))) # get rels for a single predicate r = self.rest_api.getRelationships(self.pid, predicate=self.rel_owner) graph = parse_rdf(r.content, r.url) # should include just the one we asked for self.assertEqual(1, len(list(graph))) self.assertEqual( 'johndoe', str( graph.value(subject=URIRef('info:fedora/%s' % self.pid), predicate=URIRef(self.rel_owner)))) def test_compareDatastreamChecksum(self): # create datastream with checksum (added, ds) = self._add_text_datastream() r = self.rest_api.compareDatastreamChecksum(self.pid, ds['id']) mdsum = hashlib.md5() mdsum.update(force_bytes(self.TEXT_CONTENT)) text_md5 = mdsum.hexdigest() self.assert_('<dsChecksum>%s</dsChecksum>' % text_md5 in r.text) # FIXME: how to test that checksum has actually been checked? # check for log message in audit trail r = self.rest_api.getObjectXML(self.pid) self.assert_(ds['logMessage'] in r.text) def test_export(self): r = self.rest_api.export(self.pid) export = r.text self.assert_('<foxml:datastream' in export) self.assert_('PID="%s"' % self.pid in export) self.assert_('<foxml:property' in export) self.assert_('<foxml:datastream ID="DC" ' in export) # default 'context' is public; also test migrate & archive # FIXME/TODO: add more datastreams/versions so export formats differ ? r = self.rest_api.export(self.pid, context="migrate") self.assert_('<foxml:datastream' in r.text) r = self.rest_api.export(self.pid, context="archive") self.assert_('<foxml:datastream' in r.text) # bogus id self.assertRaises(Exception, self.rest_api.export, "bogus:pid") def test_getDatastream(self): r = self.rest_api.getDatastream(self.pid, "DC") ds_profile = r.content ds_profile_text = r.text self.assert_('<datastreamProfile' in ds_profile_text) self.assert_('pid="%s"' % self.pid in ds_profile_text) self.assert_('dsID="DC"' in ds_profile_text) self.assert_('<dsLabel>Dublin Core</dsLabel>' in ds_profile_text) self.assert_('<dsVersionID>DC.0</dsVersionID>' in ds_profile_text) self.assert_('<dsCreateDate>%s' % self.today in ds_profile_text) self.assert_('<dsState>A</dsState>' in ds_profile_text) self.assert_('<dsMIME>text/xml</dsMIME>' in ds_profile_text) self.assert_('<dsControlGroup>X</dsControlGroup>' in ds_profile_text) self.assert_('<dsVersionable>true</dsVersionable>' in ds_profile_text) # get server datetime param (the hard way) for testing dsprofile_node = etree.fromstring(ds_profile, base_url=r.url) created_s = dsprofile_node.xpath('string(m:dsCreateDate)', namespaces={'m': FEDORA_MANAGE_NS}) created = fedoratime_to_datetime(created_s) # with date param r = self.rest_api.getDatastream(self.pid, "DC", asOfDateTime=created + ONE_SEC) ds_profile_now = r.text # NOTE: contents are not exactly equal because 'now' version includes a dateTime attribute self.assert_('<dsLabel>Dublin Core</dsLabel>' in ds_profile_now) self.assert_('<dsVersionID>DC.0</dsVersionID>' in ds_profile_now) # bogus datastream id on valid pid self.assertRaises(Exception, self.rest_api.getDatastream, self.pid, "BOGUS") # bogus pid self.assertRaises(Exception, self.rest_api.getDatastream, "bogus:pid", "DC") def test_getDatastreamHistory(self): r = self.rest_api.getDatastreamHistory(self.pid, "DC") # default format is html self.assert_('<h3>Datastream History View</h3>' in r.text) r = self.rest_api.getDatastreamHistory(self.pid, "DC", format='xml') data = r.text # check various pieces of datastream info self.assert_('<dsVersionID>DC.0</dsVersionID>' in data) self.assert_('<dsControlGroup>X</dsControlGroup>' in data) self.assert_('<dsLabel>Dublin Core</dsLabel>' in data) self.assert_('<dsVersionable>true</dsVersionable>' in data) self.assert_('<dsMIME>text/xml</dsMIME>' in data) self.assert_('<dsState>A</dsState>' in data) # modify DC so there are multiple versions new_dc = """<oai_dc:dc xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:oai_dc='http://www.openarchives.org/OAI/2.0/oai_dc/'> <dc:title>Test-Object</dc:title> <dc:description>modified!</dc:description> </oai_dc:dc>""" self.rest_api.modifyDatastream(self.pid, "DC", "DCv2Dublin Core", mimeType="text/xml", logMessage="updating DC", content=new_dc) r = self.rest_api.getDatastreamHistory(self.pid, 'DC', format='xml') data = r.text # should include both versions self.assert_('<dsVersionID>DC.0</dsVersionID>' in data) self.assert_('<dsVersionID>DC.1</dsVersionID>' in data) # bogus datastream self.assertRaises(RequestFailed, self.rest_api.getDatastreamHistory, self.pid, "BOGUS") # bogus pid self.assertRaises(RequestFailed, self.rest_api.getDatastreamHistory, "bogus:pid", "DC") def test_getNextPID(self): r = self.rest_api.getNextPID() self.assert_('<pidList' in r.text) self.assert_('<pid>' in r.text) r = self.rest_api.getNextPID(numPIDs=3, namespace="test-ns") self.assertEqual(3, r.text.count("<pid>test-ns:")) def test_getObjectXML(self): # update the object so we can look for audit trail in object xml added, ds = self._add_text_datastream() r = self.rest_api.getObjectXML(self.pid) objxml = r.text self.assert_('<foxml:digitalObject' in objxml) self.assert_('<foxml:datastream ID="DC" ' in objxml) # audit trail accessible in full xml self.assert_('<audit:auditTrail ' in objxml) # bogus id self.assertRaises(Exception, self.rest_api.getObjectXML, "bogus:pid") def test_ingest(self): obj = self.loadFixtureData('basic-object.foxml') r = self.rest_api.ingest(obj) pid = r.content self.assertTrue(pid) self.rest_api.purgeObject(force_text(pid)) # test ingesting with log message r = self.rest_api.ingest(obj, "this is my test ingest message") pid = r.text # ingest message is stored in AUDIT datastream # - can currently only be accessed by retrieving entire object xml r = self.rest_api.getObjectXML(pid) self.assertTrue("this is my test ingest message" in r.text) self.rest_api.purgeObject(pid, "removing test ingest object") def test_modifyDatastream(self): # add a datastream to be modified added, ds = self._add_text_datastream() new_text = """Sigh no more, ladies sigh no more. Men were deceivers ever. So be you blythe and bonny, singing hey-nonny-nonny.""" FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write(new_text) FILE.flush() # modify managed datastream by file r = self.rest_api.modifyDatastream( self.pid, ds['id'], "text datastream (modified)", mimeType="text/other", logMessage="modifying TEXT datastream", content=open(FILE.name)) self.assertTrue(r.status_code == requests.codes.ok) # log message in audit trail r = self.rest_api.getObjectXML(self.pid) self.assert_('modifying TEXT datastream' in r.text) r = self.rest_api.getDatastream(self.pid, ds['id']) ds_profile = r.text self.assert_( '<dsLabel>text datastream (modified)</dsLabel>' in ds_profile) self.assert_('<dsVersionID>%s.1</dsVersionID>' % ds['id'] in ds_profile) self.assert_('<dsState>A</dsState>' in ds_profile) self.assert_('<dsMIME>text/other</dsMIME>' in ds_profile) r = self.rest_api.getDatastreamDissemination(self.pid, ds['id']) self.assertEqual(r.text, new_text) # modify DC (inline xml) by string new_dc = """<oai_dc:dc xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:oai_dc='http://www.openarchives.org/OAI/2.0/oai_dc/'> <dc:title>Test-Object</dc:title> <dc:description>modified!</dc:description> </oai_dc:dc>""" r = self.rest_api.modifyDatastream(self.pid, "DC", "Dublin Core", mimeType="text/xml", logMessage="updating DC", content=new_dc) self.assertTrue(r.status_code == requests.codes.ok) r = self.rest_api.getDatastreamDissemination(self.pid, "DC") # fedora changes whitespace in xml, so exact test fails dc = r.text self.assert_('<dc:title>Test-Object</dc:title>' in dc) self.assert_('<dc:description>modified!</dc:description>' in dc) # invalid checksum self.assertRaises(ChecksumMismatch, self.rest_api.modifyDatastream, self.pid, "DC", "Dublin Core", mimeType="text/xml", logMessage="updating DC", content=new_dc, checksum='totally-bogus-not-even-an-MD5', checksumType='MD5') # bogus datastream on valid pid self.assertRaises(RequestFailed, self.rest_api.modifyDatastream, self.pid, "BOGUS", "Text DS", mimeType="text/plain", logMessage="modifiying non-existent DS", content=open(FILE.name)) # bogus pid self.assertRaises(RequestFailed, self.rest_api.modifyDatastream, "bogus:pid", "TEXT", "Text DS", mimeType="text/plain", logMessage="modifiying non-existent DS", content=open(FILE.name)) FILE.close() def test_modifyObject(self): r = self.rest_api.modifyObject(self.pid, "modified test object", "testuser", "I", "testing modify object") modified = (r.status_code == requests.codes.ok) self.assertTrue(modified) # log message in audit trail r = self.rest_api.getObjectXML(self.pid) self.assert_('testing modify object' in r.text) r = self.rest_api.getObjectProfile(self.pid) profile = r.text self.assert_('<objLabel>modified test object</objLabel>' in profile) self.assert_('<objOwnerId>testuser</objOwnerId>' in profile) self.assert_('<objState>I</objState>' in profile) # bogus id self.assertRaises(RequestFailed, self.rest_api.modifyObject, "bogus:pid", "modified test object", "testuser", "I", "testing modify object") def test_purgeDatastream(self): # add a datastream that can be purged (added, dsprofile), ds = self._add_text_datastream() # grab datastream creation date from addDatastream result to test purge result dsprofile_node = etree.fromstring(dsprofile) created = dsprofile_node.xpath('string(m:dsCreateDate)', namespaces={'m': FEDORA_MANAGE_NS}) # purgeDatastream gives us back the time in a different format: expect_created = created if expect_created.endswith('Z'): # it does # strip of the Z and any final zeros expect_created = expect_created.rstrip('Z0') # strip the decimal if it got that far expect_created = expect_created.rstrip('.') # and put back the Z expect_created += 'Z' r = self.rest_api.purgeDatastream(self.pid, ds['id'], logMessage="purging text datastream") purged = (r.status_code == requests.codes.ok) times = r.text self.assertTrue(purged) self.assert_(expect_created in times, 'datastream creation date should be returned in list of purged datastreams - expected %s, got %s' % \ (expect_created, times)) # log message in audit trail r = self.rest_api.getObjectXML(self.pid) self.assert_('purging text datastream' in r.text) # datastream no longer listed r = self.rest_api.listDatastreams(self.pid) self.assert_('<datastream dsid="%s"' % ds['id'] not in r.text) # NOTE: Fedora bug - attempting to purge a non-existent datastream returns 204? # purged = self.rest_api.purgeDatastream(self.pid, "BOGUS", # logMessage="test purging non-existent datastream") # self.assertFalse(purged) self.assertRaises( RequestFailed, self.rest_api.purgeDatastream, "bogus:pid", "BOGUS", logMessage= "test purging non-existent datastream from non-existent object") # also test purging specific versions of a datastream ? # attempt to purge a version that doesn't exist (added, dsprofile), ds = self._add_text_datastream() tomorrow = datetime.now(tzutc()) + timedelta(1) r = self.rest_api.purgeDatastream( self.pid, ds['id'], startDT=datetime_to_fedoratime(tomorrow), logMessage="purging text datastream") success = (r.status_code == requests.codes.ok) times = r.text # no errors, no versions purged self.assertTrue(success) self.assertEqual('[]', times) def test_purgeObject(self): obj = load_fixture_data('basic-object.foxml') r = self.rest_api.ingest(obj) pid = r.text r = self.rest_api.purgeObject(pid) purged = (r.status_code == requests.codes.ok) self.assertTrue(purged) # NOTE: fedora doesn't notice the object has been purged right away sleep( 7 ) # 5-6 was fastest this worked; padding to avoid spurious failures self.assertRaises(Exception, self.rest_api.getObjectProfile, pid) # bad pid self.assertRaises(RequestFailed, self.rest_api.purgeObject, "bogus:pid") def test_purgeRelationship(self): # add relation to purg self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, predicate=force_text(modelns.hasModel), object='info:fedora/pid:123') print(self.pid) print(force_text(self.pid)) print(type(self.pid)) print(self.fedora_fixtures_ingested) purged = self.rest_api.purgeRelationship(self.pid, 'info:fedora/%s' % self.pid, force_text(modelns.hasModel), 'info:fedora/pid:123') self.assertEqual(purged, True) # purge non-existent rel on valid pid purged = self.rest_api.purgeRelationship(self.pid, 'info:fedora/%s' % self.pid, self.rel_owner, 'johndoe', isLiteral=True) self.assertFalse(purged) # bogus pid self.assertRaises(RequestFailed, self.rest_api.purgeRelationship, "bogus:pid", 'info:fedora/bogus:pid', self.rel_owner, "johndoe", True) def test_setDatastreamState(self): # in Fedora 3.5, Fedora returns a BadRequest when we attempt to # mark DC as inactive (probably reasonable); testing on a # non-required datastream instead. (added, dsprofile), ds = self._add_text_datastream() set_state = self.rest_api.setDatastreamState(self.pid, "TEXT", "I") self.assertTrue(set_state) # get datastream to confirm change r = self.rest_api.getDatastream(self.pid, "TEXT") self.assert_('<dsState>I</dsState>' in r.text) # bad datastream id self.assertRaises(RequestFailed, self.rest_api.setDatastreamState, self.pid, "BOGUS", "I") # non-existent pid self.assertRaises(RequestFailed, self.rest_api.setDatastreamState, "bogus:pid", "DC", "D") def test_setDatastreamVersionable(self): # In Fedora 3.5, Fedora returns a BadRequest when we attempt # to change DC versionable (reasonable?); testing on a # non-required datastream instead. (added, dsprofile), ds = self._add_text_datastream() set_versioned = self.rest_api.setDatastreamVersionable( self.pid, "TEXT", False) self.assertTrue(set_versioned) # get datastream profile to confirm change r = self.rest_api.getDatastream(self.pid, "TEXT") self.assert_('<dsVersionable>false</dsVersionable>' in r.text) # bad datastream id self.assertRaises(RequestFailed, self.rest_api.setDatastreamVersionable, self.pid, "BOGUS", False) # non-existent pid self.assertRaises(RequestFailed, self.rest_api.setDatastreamVersionable, "bogus:pid", "DC", True) # utility methods def test_upload_string(self): data = "Here is some temporary content to upload to fedora." upload_id = self.rest_api.upload(data) # current format looks like uploaded://#### pattern = re.compile('uploaded://[0-9]+') self.assert_(pattern.match(force_text(upload_id))) def test_upload_file(self): FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write("Here is some temporary content to upload to fedora.") FILE.flush() with open(FILE.name, 'rb') as f: upload_id = self.rest_api.upload(f) # current format looks like uploaded://#### pattern = re.compile('uploaded://[0-9]+') self.assert_(pattern.match(upload_id)) self.assertTrue(pattern.match(upload_id)) def test_upload_generator(self): # test uploading content from a generator def data_generator(): yield 'line one of text\n' yield 'line two of text\n' yield 'line three of text\n' text_content = ''.join(data_generator()) content_md5 = md5sum(text_content) size = len(text_content) upload_id = self.rest_api.upload(data_generator(), size=size, content_type='text/plain') pattern = re.compile('uploaded://[0-9]+') self.assertTrue(pattern.match(upload_id)) # check that the *right* content was uploaded by adding # a datastream using the computed MD5 and generated upload id obj = load_fixture_data('basic-object.foxml') response = self.rest_api.ingest(obj) pid = response.text add_response = self.rest_api.addDatastream(pid, 'text', controlGroup='M', dsLocation=upload_id, mimeType='text/plain', checksumType='MD5', checksum=content_md5) self.assertTrue(add_response.status_code, requests.codes.created) # get the content from fedora and confirm it matches what was sent dsresponse = self.rest_api.getDatastreamDissemination(pid, 'text') self.assertEqual(text_content, dsresponse.text) # clean up test object self.rest_api.purgeObject(pid)
def setUp(self): super(TestREST_API, self).setUp() self.pid = self.fedora_fixtures_ingested[0] self.opener = AuthorizingServerConnection(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD) self.rest_api = REST_API(self.opener) self.today = datetime.utcnow().date()
class TestREST_API(FedoraTestCase): fixtures = ["object-with-pid.foxml"] pidspace = FEDORA_PIDSPACE TEXT_CONTENT = """This is my text content for a new datastream. Hey, nonny-nonny.""" def _add_text_datastream(self): # add a text datastream to the current test object - used by multiple tests FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write(self.TEXT_CONTENT) FILE.flush() # info for calling addDatastream, and return ds = { "id": "TEXT", "label": "text datastream", "mimeType": "text/plain", "controlGroup": "M", "logMessage": "creating new datastream", "checksumType": "MD5", } added = self.rest_api.addDatastream( self.pid, ds["id"], ds["label"], ds["mimeType"], ds["logMessage"], ds["controlGroup"], filename=FILE.name, checksumType=ds["checksumType"], ) FILE.close() return (added, ds) def setUp(self): super(TestREST_API, self).setUp() self.pid = self.fedora_fixtures_ingested[0] self.opener = AuthorizingServerConnection(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD) self.rest_api = REST_API(self.opener) self.today = datetime.utcnow().date() # API-A calls def test_findObjects(self): # search for current test object (restrict to current pidspace to avoid bogus failures) found, url = self.rest_api.findObjects("ownerId~tester pid~%s:*" % FEDORA_PIDSPACE) self.assert_("<result " in found) self.assert_("<resultList>" in found) self.assert_("<pid>%s</pid>" % self.pid in found) # crazy search that shouldn't match anything found = self.rest_api.findObjects("title~supercalifragilistexpi...") self.assert_("<objectFields>" not in found) # search for everything - get enough results to get a session token # - note that current test fedora includes a number of control objects # - using smaller chunk size to ensure pagination found, url = self.rest_api.findObjects("title~*", chunksize=2) self.assert_("<listSession>" in found) self.assert_("<token>" in found) # search by terms found, url = self.rest_api.findObjects(terms="more dat? in it than a *") self.assert_("<pid>%s</pid>" % self.pid in found) # NOTE: not testing resumeFind here because it would require parsing the xml # for the session token - tested at the server/Repository level def test_getDatastreamDissemination(self): dc, url = self.rest_api.getDatastreamDissemination(self.pid, "DC") self.assert_("<dc:title>A partially-prepared test object</dc:title>" in dc) self.assert_("<dc:description>This object has more data" in dc) self.assert_("<dc:identifier>%s</dc:identifier>" % self.pid in dc) # get server datetime param (the hard way) for testing dsprofile_data, url = self.rest_api.getDatastream(self.pid, "DC") dsprofile_node = etree.fromstring(dsprofile_data, base_url=url) created_s = dsprofile_node.xpath("string(m:dsCreateDate)", namespaces={"m": FEDORA_MANAGE_NS}) created = fedoratime_to_datetime(created_s) # with date-time param postcreate_dc, url = self.rest_api.getDatastreamDissemination(self.pid, "DC", asOfDateTime=created + ONE_SEC) self.assertEqual(dc, postcreate_dc) # unchanged within its first sec # bogus datastream self.assertRaises(Exception, self.rest_api.getDatastreamDissemination, self.pid, "BOGUS") # bogus pid self.assertRaises(Exception, self.rest_api.getDatastreamDissemination, "bogus:pid", "BOGUS") # return_http_response response = self.rest_api.getDatastreamDissemination(self.pid, "DC", return_http_response=True) self.assert_( isinstance(response, httplib.HTTPResponse), "getDatastreamDissemination should return an HTTPResponse when return_http_response is True", ) # datastream content should still be accessible self.assertEqual(dc, response.read()) # NOTE: getDissemination not available in REST API until Fedora 3.3 def test_getDissemination(self): # testing with built-in fedora dissemination profile, uri = self.rest_api.getDissemination(self.pid, "fedora-system:3", "viewItemIndex") self.assert_("<title>Object Items HTML Presentation</title>" in profile) self.assert_(self.pid in profile) # return_http_response response = self.rest_api.getDissemination( self.pid, "fedora-system:3", "viewItemIndex", return_http_response=True ) self.assert_( isinstance(response, httplib.HTTPResponse), "getDissemination should return an HTTPResponse when return_http_response is True", ) # datastream content should still be accessible self.assert_(self.pid in response.read()) def test_getObjectHistory(self): history, url = self.rest_api.getObjectHistory(self.pid) self.assert_("<fedoraObjectHistory" in history) self.assert_('pid="%s"' % self.pid in history) self.assert_("<objectChangeDate>%s" % self.today in history) # bogus pid self.assertRaises(Exception, self.rest_api.getObjectHistory, "bogus:pid") def test_getObjectProfile(self): profile, url = self.rest_api.getObjectProfile(self.pid) self.assert_("<objectProfile" in profile) self.assert_('pid="%s"' % self.pid in profile) self.assert_("<objLabel>A partially-prepared test object</objLabel>" in profile) self.assert_("<objOwnerId>tester</objOwnerId>" in profile) self.assert_("<objCreateDate>%s" % self.today in profile) self.assert_("<objLastModDate>%s" % self.today in profile) self.assert_("<objState>A</objState>" in profile) # unchecked: objDissIndexViewURL, objItemIndexViewURL # get server datetime param (the hard way) for testing profile_node = etree.fromstring(profile, base_url=url) created_s = profile_node.xpath("string(a:objCreateDate)", namespaces={"a": FEDORA_ACCESS_NS}) created = fedoratime_to_datetime(created_s) # with time profile_now, url = self.rest_api.getObjectProfile(self.pid, asOfDateTime=created + ONE_SEC) # NOTE: profile content is not exactly the same because it includes a datetime attribute self.assert_('pid="%s"' % self.pid in profile_now) self.assert_("<objLabel>A partially-prepared test object</objLabel>" in profile_now) # bogus pid self.assertRaises(Exception, self.rest_api.getObjectHistory, "bogus:pid") def test_listDatastreams(self): dslist, url = self.rest_api.listDatastreams(self.pid) self.assert_("<objectDatastreams" in dslist) self.assert_('<datastream dsid="DC" label="Dublin Core" mimeType="text/xml"' in dslist) # bogus pid self.assertRaises(Exception, self.rest_api.listDatastreams, "bogus:pid") def test_listMethods(self): methods, url = self.rest_api.listMethods(self.pid) self.assert_("<objectMethods" in methods) self.assert_('pid="%s"' % self.pid in methods) # default fedora methods, should be available on every object self.assert_('<sDef pid="fedora-system:3" ' in methods) self.assert_('<method name="viewObjectProfile"' in methods) self.assert_('<method name="viewItemIndex"' in methods) # methods for a specified sdef # NOTE: this causes a 404 error; fedora bug? possibly does not work with system methods? # methods = self.rest_api.listMethods(self.pid, 'fedora-system:3') self.assertRaises(Exception, self.rest_api.listMethods, "bogus:pid") # API-M calls def test_addDatastream(self): # returns result from addDatastream call and info used for add ((added, msg), ds) = self._add_text_datastream() self.assertTrue(added) # response from addDatastream message, url = self.rest_api.getObjectXML(self.pid) self.assert_(ds["logMessage"] in message) dslist, url = self.rest_api.listDatastreams(self.pid) self.assert_('<datastream dsid="%(id)s" label="%(label)s" mimeType="%(mimeType)s" />' % ds in dslist) ds_profile, url = self.rest_api.getDatastream(self.pid, ds["id"]) self.assert_('dsID="%s" ' % ds["id"] in ds_profile) self.assert_("<dsLabel>%s</dsLabel>" % ds["label"] in ds_profile) self.assert_("<dsVersionID>%s.0</dsVersionID>" % ds["id"] in ds_profile) self.assert_("<dsCreateDate>%s" % self.today in ds_profile) self.assert_("<dsState>A</dsState>" in ds_profile) self.assert_("<dsMIME>%s</dsMIME>" % ds["mimeType"] in ds_profile) self.assert_("<dsControlGroup>%s</dsControlGroup>" % ds["controlGroup"] in ds_profile) self.assert_("<dsVersionable>true</dsVersionable>" in ds_profile) # content returned from fedora should be exactly what we started with ds_content, url = self.rest_api.getDatastreamDissemination(self.pid, ds["id"]) self.assertEqual(self.TEXT_CONTENT, ds_content) # invalid checksum self.assertRaises( ChecksumMismatch, self.rest_api.addDatastream, self.pid, "TEXT2", "text datastream", mimeType="text/plain", logMessage="creating TEXT2", content="<some> text content</some>", checksum="totally-bogus-not-even-an-MD5", checksumType="MD5", ) # invalid checksum without a checksum type - warning, but no checksum mismatch with warnings.catch_warnings(record=True) as w: self.rest_api.addDatastream( self.pid, "TEXT2", "text datastream", mimeType="text/plain", logMessage="creating TEXT2", content="<some> text content</some>", checksum="totally-bogus-not-even-an-MD5", checksumType=None, ) self.assertEqual( 1, len(w), "calling addDatastream with checksum but no checksum type should generate a warning" ) self.assert_("Fedora will ignore the checksum" in str(w[0].message)) # attempt to add to a non-existent object FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write("bogus") FILE.flush() self.assertRaises( RequestFailed, self.rest_api.addDatastream, "bogus:pid", "TEXT", "text datastream", mimeType="text/plain", logMessage="creating new datastream", controlGroup="M", filename=FILE.name, ) FILE.close() # relationship predicates for testing rel_isMemberOf = "info:fedora/fedora-system:def/relations-external#isMemberOf" rel_owner = "info:fedora/fedora-system:def/relations-external#owner" def test_addRelationship(self): # rel to resource added = self.rest_api.addRelationship( self.pid, "info:fedora/%s" % self.pid, unicode(modelns.hasModel), "info:fedora/pid:123", False ) self.assertTrue(added) rels, url = self.rest_api.getDatastreamDissemination(self.pid, "RELS-EXT") self.assert_("<hasModel" in rels) self.assert_('rdf:resource="info:fedora/pid:123"' in rels) # literal added = self.rest_api.addRelationship(self.pid, "info:fedora/%s" % self.pid, self.rel_owner, "johndoe", True) self.assertTrue(added) rels, url = self.rest_api.getDatastreamDissemination(self.pid, "RELS-EXT") self.assert_("<owner" in rels) self.assert_(">johndoe<" in rels) # bogus pid self.assertRaises( RequestFailed, self.rest_api.addRelationship, "bogus:pid", "info:fedora/bogus:pid", self.rel_owner, "johndoe", True, ) def test_getRelationships(self): # add relations to retrieve self.rest_api.addRelationship( self.pid, "info:fedora/%s" % self.pid, unicode(modelns.hasModel), "info:fedora/pid:123", False ) self.rest_api.addRelationship(self.pid, "info:fedora/%s" % self.pid, self.rel_owner, "johndoe", True) data, url = self.rest_api.getRelationships(self.pid) graph = parse_rdf(data, url) # check total number: fedora-system cmodel + two just added self.assertEqual(3, len(list(graph))) # newly added triples should be included in the graph self.assert_((URIRef("info:fedora/%s" % self.pid), modelns.hasModel, URIRef("info:fedora/pid:123")) in graph) self.assertEqual( "johndoe", graph.value(subject=URIRef("info:fedora/%s" % self.pid), predicate=URIRef(self.rel_owner)) ) # get rels for a single predicate data, url = self.rest_api.getRelationships(self.pid, predicate=self.rel_owner) graph = parse_rdf(data, url) # should include just the one we asked for self.assertEqual(1, len(list(graph))) self.assertEqual( "johndoe", graph.value(subject=URIRef("info:fedora/%s" % self.pid), predicate=URIRef(self.rel_owner)) ) def test_compareDatastreamChecksum(self): # create datastream with checksum (added, ds) = self._add_text_datastream() ds_info, pid = self.rest_api.compareDatastreamChecksum(self.pid, ds["id"]) mdsum = md5.new() mdsum.update(self.TEXT_CONTENT) text_md5 = mdsum.hexdigest() self.assert_("<dsChecksum>%s</dsChecksum>" % text_md5 in ds_info) # FIXME: how to test that checksum has actually been checked? # check for log message in audit trail xml, url = self.rest_api.getObjectXML(self.pid) self.assert_(ds["logMessage"] in xml) def test_export(self): export, url = self.rest_api.export(self.pid) self.assert_("<foxml:datastream" in export) self.assert_('PID="%s"' % self.pid in export) self.assert_("<foxml:property" in export) self.assert_('<foxml:datastream ID="DC" ' in export) # default 'context' is public; also test migrate & archive # FIXME/TODO: add more datastreams/versions so export formats differ ? export, url = self.rest_api.export(self.pid, context="migrate") self.assert_("<foxml:datastream" in export) export, url = self.rest_api.export(self.pid, context="archive") self.assert_("<foxml:datastream" in export) # bogus id self.assertRaises(Exception, self.rest_api.export, "bogus:pid") def test_getDatastream(self): ds_profile, url = self.rest_api.getDatastream(self.pid, "DC") self.assert_("<datastreamProfile" in ds_profile) self.assert_('pid="%s"' % self.pid in ds_profile) self.assert_('dsID="DC" ' in ds_profile) self.assert_("<dsLabel>Dublin Core</dsLabel>" in ds_profile) self.assert_("<dsVersionID>DC.0</dsVersionID>" in ds_profile) self.assert_("<dsCreateDate>%s" % self.today in ds_profile) self.assert_("<dsState>A</dsState>" in ds_profile) self.assert_("<dsMIME>text/xml</dsMIME>" in ds_profile) self.assert_("<dsControlGroup>X</dsControlGroup>" in ds_profile) self.assert_("<dsVersionable>true</dsVersionable>" in ds_profile) # get server datetime param (the hard way) for testing dsprofile_node = etree.fromstring(ds_profile, base_url=url) created_s = dsprofile_node.xpath("string(m:dsCreateDate)", namespaces={"m": FEDORA_MANAGE_NS}) created = fedoratime_to_datetime(created_s) # with date param ds_profile_now, url = self.rest_api.getDatastream(self.pid, "DC", asOfDateTime=created + ONE_SEC) # NOTE: contents are not exactly equal because 'now' version includes a dateTime attribute self.assert_("<dsLabel>Dublin Core</dsLabel>" in ds_profile_now) self.assert_("<dsVersionID>DC.0</dsVersionID>" in ds_profile_now) # bogus datastream id on valid pid self.assertRaises(Exception, self.rest_api.getDatastream, self.pid, "BOGUS") # bogus pid self.assertRaises(Exception, self.rest_api.getDatastream, "bogus:pid", "DC") def test_getDatastreamHistory(self): data, url = self.rest_api.getDatastreamHistory(self.pid, "DC") # default format is html self.assert_("<h3>Datastream History View</h3>" in data) data, url = self.rest_api.getDatastreamHistory(self.pid, "DC", format="xml") # check various pieces of datastream info self.assert_("<dsVersionID>DC.0</dsVersionID>" in data) self.assert_("<dsControlGroup>X</dsControlGroup>" in data) self.assert_("<dsLabel>Dublin Core</dsLabel>" in data) self.assert_("<dsVersionable>true</dsVersionable>" in data) self.assert_("<dsMIME>text/xml</dsMIME>" in data) self.assert_("<dsState>A</dsState>" in data) # modify DC so there are multiple versions new_dc = """<oai_dc:dc xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:oai_dc='http://www.openarchives.org/OAI/2.0/oai_dc/'> <dc:title>Test-Object</dc:title> <dc:description>modified!</dc:description> </oai_dc:dc>""" self.rest_api.modifyDatastream( self.pid, "DC", "DCv2Dublin Core", mimeType="text/xml", logMessage="updating DC", content=new_dc ) data, url = self.rest_api.getDatastreamHistory(self.pid, "DC", format="xml") # should include both versions self.assert_("<dsVersionID>DC.0</dsVersionID>" in data) self.assert_("<dsVersionID>DC.1</dsVersionID>" in data) # bogus datastream self.assertRaises(RequestFailed, self.rest_api.getDatastreamHistory, self.pid, "BOGUS") # bogus pid self.assertRaises(RequestFailed, self.rest_api.getDatastreamHistory, "bogus:pid", "DC") def test_getNextPID(self): pids, url = self.rest_api.getNextPID() self.assert_("<pidList" in pids) self.assert_("<pid>" in pids) pids, url = self.rest_api.getNextPID(numPIDs=3, namespace="test-ns") self.assertEqual(3, pids.count("<pid>test-ns:")) def test_getObjectXML(self): # update the object so we can look for audit trail in object xml added, ds = self._add_text_datastream() objxml, url = self.rest_api.getObjectXML(self.pid) self.assert_("<foxml:digitalObject" in objxml) self.assert_('<foxml:datastream ID="DC" ' in objxml) # audit trail accessible in full xml self.assert_("<audit:auditTrail " in objxml) # bogus id self.assertRaises(Exception, self.rest_api.getObjectXML, "bogus:pid") def test_ingest(self): object = load_fixture_data("basic-object.foxml") pid = self.rest_api.ingest(object) self.assertTrue(pid) self.rest_api.purgeObject(pid) # test ingesting with log message pid = self.rest_api.ingest(object, "this is my test ingest message") # ingest message is stored in AUDIT datastream # - can currently only be accessed by retrieving entire object xml xml, url = self.rest_api.getObjectXML(pid) self.assertTrue("this is my test ingest message" in xml) self.rest_api.purgeObject(pid, "removing test ingest object") def test_modifyDatastream(self): # add a datastream to be modified added, ds = self._add_text_datastream() new_text = """Sigh no more, ladies sigh no more. Men were deceivers ever. So be you blythe and bonny, singing hey-nonny-nonny.""" FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write(new_text) FILE.flush() # modify managed datastream by file updated, msg = self.rest_api.modifyDatastream( self.pid, ds["id"], "text datastream (modified)", mimeType="text/other", logMessage="modifying TEXT datastream", content=open(FILE.name), ) self.assertTrue(updated) # log message in audit trail xml, url = self.rest_api.getObjectXML(self.pid) self.assert_("modifying TEXT datastream" in xml) ds_profile, url = self.rest_api.getDatastream(self.pid, ds["id"]) self.assert_("<dsLabel>text datastream (modified)</dsLabel>" in ds_profile) self.assert_("<dsVersionID>%s.1</dsVersionID>" % ds["id"] in ds_profile) self.assert_("<dsState>A</dsState>" in ds_profile) self.assert_("<dsMIME>text/other</dsMIME>" in ds_profile) content, url = self.rest_api.getDatastreamDissemination(self.pid, ds["id"]) self.assertEqual(content, new_text) # modify DC (inline xml) by string new_dc = """<oai_dc:dc xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:oai_dc='http://www.openarchives.org/OAI/2.0/oai_dc/'> <dc:title>Test-Object</dc:title> <dc:description>modified!</dc:description> </oai_dc:dc>""" updated, msg = self.rest_api.modifyDatastream( self.pid, "DC", "Dublin Core", mimeType="text/xml", logMessage="updating DC", content=new_dc ) self.assertTrue(updated) dc, url = self.rest_api.getDatastreamDissemination(self.pid, "DC") # fedora changes whitespace in xml, so exact test fails self.assert_("<dc:title>Test-Object</dc:title>" in dc) self.assert_("<dc:description>modified!</dc:description>" in dc) # invalid checksum self.assertRaises( ChecksumMismatch, self.rest_api.modifyDatastream, self.pid, "DC", "Dublin Core", mimeType="text/xml", logMessage="updating DC", content=new_dc, checksum="totally-bogus-not-even-an-MD5", checksumType="MD5", ) # bogus datastream on valid pid self.assertRaises( RequestFailed, self.rest_api.modifyDatastream, self.pid, "BOGUS", "Text DS", mimeType="text/plain", logMessage="modifiying non-existent DS", content=open(FILE.name), ) # bogus pid self.assertRaises( RequestFailed, self.rest_api.modifyDatastream, "bogus:pid", "TEXT", "Text DS", mimeType="text/plain", logMessage="modifiying non-existent DS", content=open(FILE.name), ) FILE.close() def test_modifyObject(self): modified = self.rest_api.modifyObject( self.pid, "modified test object", "testuser", "I", "testing modify object" ) self.assertTrue(modified) # log message in audit trail xml, url = self.rest_api.getObjectXML(self.pid) self.assert_("testing modify object" in xml) profile, xml = self.rest_api.getObjectProfile(self.pid) self.assert_("<objLabel>modified test object</objLabel>" in profile) self.assert_("<objOwnerId>testuser</objOwnerId>" in profile) self.assert_("<objState>I</objState>" in profile) # bogus id self.assertRaises( RequestFailed, self.rest_api.modifyObject, "bogus:pid", "modified test object", "testuser", "I", "testing modify object", ) def test_purgeDatastream(self): # add a datastream that can be purged (added, dsprofile), ds = self._add_text_datastream() # grab datastream creation date from addDatastream result to test purge result dsprofile_node = etree.fromstring(dsprofile) created = dsprofile_node.xpath("string(m:dsCreateDate)", namespaces={"m": FEDORA_MANAGE_NS}) # purgeDatastream gives us back the time in a different format: expect_created = created if expect_created.endswith("Z"): # it does # strip of the Z and any final zeros expect_created = expect_created.rstrip("Z0") # strip the decimal if it got that far expect_created = expect_created.rstrip(".") # and put back the Z expect_created += "Z" purged, times = self.rest_api.purgeDatastream(self.pid, ds["id"], logMessage="purging text datastream") self.assertTrue(purged) self.assert_( expect_created in times, "datastream creation date should be returned in list of purged datastreams - expected %s, got %s" % (expect_created, times), ) # log message in audit trail xml, url = self.rest_api.getObjectXML(self.pid) self.assert_("purging text datastream" in xml) # datastream no longer listed dslist, url = self.rest_api.listDatastreams(self.pid) self.assert_('<datastream dsid="%s"' % ds["id"] not in dslist) # NOTE: Fedora bug - attempting to purge a non-existent datastream returns 204? # purged = self.rest_api.purgeDatastream(self.pid, "BOGUS", # logMessage="test purging non-existent datastream") # self.assertFalse(purged) self.assertRaises( RequestFailed, self.rest_api.purgeDatastream, "bogus:pid", "BOGUS", logMessage="test purging non-existent datastream from non-existent object", ) # also test purging specific versions of a datastream ? # attempt to purge a version that doesn't exist (added, dsprofile), ds = self._add_text_datastream() tomorrow = datetime.now(tzutc()) + timedelta(1) success, times = self.rest_api.purgeDatastream( self.pid, ds["id"], startDT=datetime_to_fedoratime(tomorrow), logMessage="purging text datastream" ) # no errors, no versions purged self.assertTrue(success) self.assertEqual("[]", times) def test_purgeObject(self): object = load_fixture_data("basic-object.foxml") pid = self.rest_api.ingest(object) purged, message = self.rest_api.purgeObject(pid) self.assertTrue(purged) # NOTE: fedora doesn't notice the object has been purged right away sleep(7) # 5-6 was fastest this worked; padding to avoid spurious failures self.assertRaises(Exception, self.rest_api.getObjectProfile, pid) # bad pid self.assertRaises(RequestFailed, self.rest_api.purgeObject, "bogus:pid") def test_purgeRelationship(self): # add relation to purg self.rest_api.addRelationship( self.pid, "info:fedora/%s" % self.pid, predicate=unicode(modelns.hasModel), object="info:fedora/pid:123" ) purged = self.rest_api.purgeRelationship( self.pid, "info:fedora/%s" % self.pid, unicode(modelns.hasModel), "info:fedora/pid:123" ) self.assertEqual(purged, True) # purge non-existent rel on valid pid purged = self.rest_api.purgeRelationship( self.pid, "info:fedora/%s" % self.pid, self.rel_owner, "johndoe", isLiteral=True ) self.assertFalse(purged) # bogus pid self.assertRaises( RequestFailed, self.rest_api.purgeRelationship, "bogus:pid", "info:fedora/bogus:pid", self.rel_owner, "johndoe", True, ) def test_setDatastreamState(self): # in Fedora 3.5, Fedora returns a BadRequest when we attempt to # mark DC as inactive (probably reasonable); testing on a # non-required datastream instead. (added, dsprofile), ds = self._add_text_datastream() set_state = self.rest_api.setDatastreamState(self.pid, "TEXT", "I") self.assertTrue(set_state) # get datastream to confirm change ds_profile, url = self.rest_api.getDatastream(self.pid, "TEXT") self.assert_("<dsState>I</dsState>" in ds_profile) # bad datastream id self.assertRaises(RequestFailed, self.rest_api.setDatastreamState, self.pid, "BOGUS", "I") # non-existent pid self.assertRaises(RequestFailed, self.rest_api.setDatastreamState, "bogus:pid", "DC", "D") def test_setDatastreamVersionable(self): # In Fedora 3.5, Fedora returns a BadRequest when we attempt # to change DC versionable (reasonable?); testing on a # non-required datastream instead. (added, dsprofile), ds = self._add_text_datastream() set_versioned = self.rest_api.setDatastreamVersionable(self.pid, "TEXT", False) self.assertTrue(set_versioned) # get datastream profile to confirm change ds_profile, url = self.rest_api.getDatastream(self.pid, "TEXT") self.assert_("<dsVersionable>false</dsVersionable>" in ds_profile) # bad datastream id self.assertRaises(RequestFailed, self.rest_api.setDatastreamVersionable, self.pid, "BOGUS", False) # non-existent pid self.assertRaises(RequestFailed, self.rest_api.setDatastreamVersionable, "bogus:pid", "DC", True) # utility methods def test_upload_string(self): data = "Here is some temporary content to upload to fedora." upload_id = self.rest_api.upload(data) # current format looks like uploaded://#### pattern = re.compile("uploaded://[0-9]+") self.assert_(pattern.match(upload_id)) def test_upload_file(self): FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write("Here is some temporary content to upload to fedora.") FILE.flush() with open(FILE.name, "rb") as f: upload_id = self.rest_api.upload(f) # current format looks like uploaded://#### pattern = re.compile("uploaded://[0-9]+") self.assert_(pattern.match(upload_id))
def setUp(self): super(TestREST_API, self).setUp() self.pid = self.fedora_fixtures_ingested[0] self.rest_api = REST_API(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD) self.today = datetime.utcnow().date()
class TestREST_API(FedoraTestCase): fixtures = ['object-with-pid.foxml'] pidspace = FEDORA_PIDSPACE TEXT_CONTENT = """This is my text content for a new datastream. Hey, nonny-nonny.""" unicode_test_str = u'«ταБЬℓσ» 🐍' def _add_text_datastream(self): # add a text datastream to the current test object - used by multiple tests FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write(self.TEXT_CONTENT) FILE.flush() # info for calling addDatastream, and return ds = {'id': 'TEXT', 'label': 'text datastream', 'mimeType': 'text/plain', 'controlGroup': 'M', 'logMessage': "creating new datastream", 'checksumType': 'MD5'} # return (r.status_code == requests.codes.created, r.content) with open(FILE.name) as data: r = self.rest_api.addDatastream(self.pid, ds['id'], ds['label'], ds['mimeType'], ds['logMessage'], ds['controlGroup'], content=data, checksumType=ds['checksumType']) FILE.close() return ((r.status_code == requests.codes.created, r.content), ds) def setUp(self): super(TestREST_API, self).setUp() self.pid = self.fedora_fixtures_ingested[0] self.rest_api = REST_API(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD) self.today = datetime.utcnow().date() # API-A calls def test_findObjects(self): # search for current test object (restrict to current pidspace to avoid bogus failures) r = self.rest_api.findObjects("ownerId~tester pid~%s:*" % FEDORA_PIDSPACE) found = r.text self.assert_('<result ' in found) self.assert_('<resultList>' in found) self.assert_('<pid>%s</pid>' % self.pid in found) # crazy search that shouldn't match anything r = self.rest_api.findObjects("title~supercalifragilistexpi...") self.assert_('<objectFields>' not in r.text) # search for everything - get enough results to get a session token # - note that current test fedora includes a number of control objects # - using smaller chunk size to ensure pagination r = self.rest_api.findObjects("title~*", chunksize=2) self.assert_('<listSession>' in r.text) self.assert_('<token>' in r.text) # search by terms r = self.rest_api.findObjects(terms="more dat? in it than a *") self.assert_('<pid>%s</pid>' % self.pid in r.text) # NOTE: not testing resumeFind here because it would require parsing the xml # for the session token - tested at the server/Repository level def test_getDatastreamDissemination(self): r = self.rest_api.getDatastreamDissemination(self.pid, "DC") dc = r.text self.assert_("<dc:title>A partially-prepared test object</dc:title>" in dc) self.assert_("<dc:description>This object has more data" in dc) self.assert_("<dc:identifier>%s</dc:identifier>" % self.pid in dc) # get server datetime param (the hard way) for testing r = self.rest_api.getDatastream(self.pid, "DC") dsprofile_node = etree.fromstring(r.content, base_url=r.url) created_s = dsprofile_node.xpath('string(m:dsCreateDate)', namespaces={'m': FEDORA_MANAGE_NS}) created = fedoratime_to_datetime(created_s) # with date-time param r = self.rest_api.getDatastreamDissemination(self.pid, "DC", asOfDateTime=created + ONE_SEC) postcreate_dc = r.text self.assertEqual(dc, postcreate_dc) # unchanged within its first sec # bogus datastream self.assertRaises(Exception, self.rest_api.getDatastreamDissemination, self.pid, "BOGUS") # bogus pid self.assertRaises(Exception, self.rest_api.getDatastreamDissemination, "bogus:pid", "BOGUS") # return_http_response response = self.rest_api.getDatastreamDissemination(self.pid, 'DC') self.assert_(isinstance(response, requests.Response), 'getDatastreamDissemination should return a response object') # datastream content should still be accessible self.assertEqual(dc, response.text) # NOTE: getDissemination not available in REST API until Fedora 3.3 def test_getDissemination(self): # testing with built-in fedora dissemination r = self.rest_api.getDissemination(self.pid, "fedora-system:3", "viewItemIndex") self.assert_('<title>Object Items HTML Presentation</title>' in r.text) self.assert_(self.pid in r.text) # return_http_response response = self.rest_api.getDissemination(self.pid, "fedora-system:3", "viewItemIndex") self.assert_(isinstance(response, requests.Response), 'getDissemination should return a response object') # datastream content should still be accessible self.assert_(self.pid in force_text(response.content)) def test_getObjectHistory(self): r = self.rest_api.getObjectHistory(self.pid) self.assert_('<fedoraObjectHistory' in r.text) self.assert_('pid="%s"' % self.pid in r.text) self.assert_('<objectChangeDate>%s' % self.today in r.text) # bogus pid self.assertRaises(Exception, self.rest_api.getObjectHistory, "bogus:pid") def test_getObjectProfile(self): r = self.rest_api.getObjectProfile(self.pid) profile = r.text self.assert_('<objectProfile' in profile) self.assert_('pid="%s"' % self.pid in profile) self.assert_('<objLabel>A partially-prepared test object</objLabel>' in profile) self.assert_('<objOwnerId>tester</objOwnerId>' in profile) self.assert_('<objCreateDate>%s' % self.today in profile) self.assert_('<objLastModDate>%s' % self.today in profile) self.assert_('<objState>A</objState>' in profile) # unchecked: objDissIndexViewURL, objItemIndexViewURL # get server datetime param (the hard way) for testing profile_node = etree.fromstring(r.content, base_url=r.url) created_s = profile_node.xpath('string(a:objCreateDate)', namespaces={'a': FEDORA_ACCESS_NS}) created = fedoratime_to_datetime(created_s) # with time r = self.rest_api.getObjectProfile(self.pid, asOfDateTime=created + ONE_SEC) profile_now = r.text # NOTE: profile content is not exactly the same because it includes a datetime attribute self.assert_('pid="%s"' % self.pid in profile_now) self.assert_('<objLabel>A partially-prepared test object</objLabel>' in profile_now) # bogus pid self.assertRaises(Exception, self.rest_api.getObjectHistory, "bogus:pid") def test_listDatastreams(self): r = self.rest_api.listDatastreams(self.pid) self.assert_('<objectDatastreams' in r.text) self.assert_('<datastream dsid="DC" label="Dublin Core" mimeType="text/xml"' in r.text) # bogus pid self.assertRaises(Exception, self.rest_api.listDatastreams, "bogus:pid") def test_listMethods(self): r = self.rest_api.listMethods(self.pid) methods = r.text self.assert_('<objectMethods' in methods) self.assert_('pid="%s"' % self.pid in methods) # default fedora methods, should be available on every object self.assert_('<sDef pid="fedora-system:3" ' in methods) self.assert_('<method name="viewObjectProfile"' in methods) self.assert_('<method name="viewItemIndex"' in methods) # methods for a specified sdef # NOTE: this causes a 404 error; fedora bug? possibly does not work with system methods? # methods = self.rest_api.listMethods(self.pid, 'fedora-system:3') self.assertRaises(Exception, self.rest_api.listMethods, "bogus:pid") # API-M calls def test_addDatastream(self): # returns result from addDatastream call and info used for add ((added, msg), ds) = self._add_text_datastream() self.assertTrue(added) # response from addDatastream r = self.rest_api.getObjectXML(self.pid) message = r.content self.assert_(ds['logMessage'] in force_text(message)) r = self.rest_api.listDatastreams(self.pid) self.assert_('<datastream dsid="%(id)s" label="%(label)s" mimeType="%(mimeType)s" />' % ds in r.text) r = self.rest_api.getDatastream(self.pid, ds['id']) ds_profile = r.text self.assert_('dsID="%s"' % ds['id'] in ds_profile) self.assert_('<dsLabel>%s</dsLabel>' % ds['label'] in ds_profile) self.assert_('<dsVersionID>%s.0</dsVersionID>' % ds['id'] in ds_profile) self.assert_('<dsCreateDate>%s' % self.today in ds_profile) self.assert_('<dsState>A</dsState>' in ds_profile) self.assert_('<dsMIME>%s</dsMIME>' % ds['mimeType'] in ds_profile) self.assert_('<dsControlGroup>%s</dsControlGroup>' % ds['controlGroup'] in ds_profile) self.assert_('<dsVersionable>true</dsVersionable>' in ds_profile) # content returned from fedora should be exactly what we started with r = self.rest_api.getDatastreamDissemination(self.pid, ds['id']) self.assertEqual(self.TEXT_CONTENT, r.text) # invalid checksum self.assertRaises( ChecksumMismatch, self.rest_api.addDatastream, self.pid, "TEXT2", "text datastream", mimeType="text/plain", logMessage="creating TEXT2", content='<some> text content</some>', checksum='totally-bogus-not-even-an-MD5', checksumType='MD5') # invalid checksum without a checksum type - warning, but no checksum mismatch with warnings.catch_warnings(record=True) as w: self.rest_api.addDatastream( self.pid, "TEXT2", "text datastream", mimeType="text/plain", logMessage="creating TEXT2", content='<some> text content</some>', checksum='totally-bogus-not-even-an-MD5', checksumType=None) self.assertEqual(1, len(w), 'calling addDatastream with checksum but no checksum type should generate a warning') self.assert_('Fedora will ignore the checksum' in str(w[0].message)) # attempt to add to a non-existent object FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write("bogus") FILE.flush() with open(FILE.name) as textfile: self.assertRaises(RequestFailed, self.rest_api.addDatastream, 'bogus:pid', 'TEXT', 'text datastream', mimeType='text/plain', logMessage='creating new datastream', controlGroup='M', content=textfile) FILE.close() def test_addDatastream_utf8(self): # unicode in datastream label response = self.rest_api.addDatastream( self.pid, "TEXT2", self.unicode_test_str, mimeType="text/plain", logMessage="creating TEXT2", controlGroup='M', versionable=False, dsState='A', content='some text content') self.assertEqual(requests.codes.created, response.status_code) # TODO: once it's fixed, add tests to confirm label is set correctly # response = self.rest_api.getDatastream(self.pid, "TEXT2") # unicode in log message response = self.rest_api.addDatastream( self.pid, "TEXT3", 'some text', mimeType="text/plain", logMessage=self.unicode_test_str, controlGroup='M', content='some text content') self.assertEqual(requests.codes.created, response.status_code) # log message should be in audit trail response = self.rest_api.getObjectXML(self.pid) response.encoding = 'utf-8' self.assert_(u'<audit:justification>%s</audit:justification>' % \ self.unicode_test_str in response.text) # unicode in datastream content response = self.rest_api.addDatastream( self.pid, "TEXT4", 'some text', mimeType="text/plain", logMessage='adding unicode content', controlGroup='M', content=self.unicode_test_str) self.assertEqual(requests.codes.created, response.status_code) # content returned from fedora should be exactly what we started with response = self.rest_api.getDatastreamDissemination(self.pid, 'TEXT4') response.encoding = 'utf-8' self.assertEqual(self.unicode_test_str, response.text) # relationship predicates for testing rel_isMemberOf = "info:fedora/fedora-system:def/relations-external#isMemberOf" rel_owner = "info:fedora/fedora-system:def/relations-external#owner" def test_addRelationship(self): # rel to resource added = self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, force_text(modelns.hasModel), 'info:fedora/pid:123', False) self.assertTrue(added) r = self.rest_api.getDatastreamDissemination(self.pid, 'RELS-EXT') self.assert_('<hasModel' in r.text) self.assert_('rdf:resource="info:fedora/pid:123"' in r.text) # literal added = self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, self.rel_owner, "johndoe", True) self.assertTrue(added) r = self.rest_api.getDatastreamDissemination(self.pid, 'RELS-EXT') self.assert_('<owner' in r.text) self.assert_('>johndoe<' in r.text) # bogus pid self.assertRaises(RequestFailed, self.rest_api.addRelationship, 'bogus:pid', 'info:fedora/bogus:pid', self.rel_owner, 'johndoe', True) def test_getRelationships(self): # add relations to retrieve self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, force_text(modelns.hasModel), "info:fedora/pid:123", False) self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, self.rel_owner, "johndoe", True) r = self.rest_api.getRelationships(self.pid) graph = parse_rdf(r.content, r.url) # check total number: fedora-system cmodel + two just added self.assertEqual(3, len(list(graph))) # newly added triples should be included in the graph self.assert_((URIRef('info:fedora/%s' % self.pid), modelns.hasModel, URIRef('info:fedora/pid:123')) in graph) self.assertEqual('johndoe', str(graph.value(subject=URIRef('info:fedora/%s' % self.pid), predicate=URIRef(self.rel_owner)))) # get rels for a single predicate r = self.rest_api.getRelationships(self.pid, predicate=self.rel_owner) graph = parse_rdf(r.content, r.url) # should include just the one we asked for self.assertEqual(1, len(list(graph))) self.assertEqual('johndoe', str(graph.value(subject=URIRef('info:fedora/%s' % self.pid), predicate=URIRef(self.rel_owner)))) def test_compareDatastreamChecksum(self): # create datastream with checksum (added, ds) = self._add_text_datastream() r = self.rest_api.compareDatastreamChecksum(self.pid, ds['id']) mdsum = hashlib.md5() mdsum.update(force_bytes(self.TEXT_CONTENT)) text_md5 = mdsum.hexdigest() self.assert_('<dsChecksum>%s</dsChecksum>' % text_md5 in r.text) # FIXME: how to test that checksum has actually been checked? # check for log message in audit trail r = self.rest_api.getObjectXML(self.pid) self.assert_(ds['logMessage'] in r.text) def test_export(self): r = self.rest_api.export(self.pid) export = r.text self.assert_('<foxml:datastream' in export) self.assert_('PID="%s"' % self.pid in export) self.assert_('<foxml:property' in export) self.assert_('<foxml:datastream ID="DC" ' in export) # default 'context' is public; also test migrate & archive # FIXME/TODO: add more datastreams/versions so export formats differ ? r = self.rest_api.export(self.pid, context="migrate") self.assert_('<foxml:datastream' in r.text) r = self.rest_api.export(self.pid, context="archive") self.assert_('<foxml:datastream' in r.text) # bogus id self.assertRaises(Exception, self.rest_api.export, "bogus:pid") def test_getDatastream(self): r = self.rest_api.getDatastream(self.pid, "DC") ds_profile = r.content ds_profile_text = r.text self.assert_('<datastreamProfile' in ds_profile_text) self.assert_('pid="%s"' % self.pid in ds_profile_text) self.assert_('dsID="DC"' in ds_profile_text) self.assert_('<dsLabel>Dublin Core</dsLabel>' in ds_profile_text) self.assert_('<dsVersionID>DC.0</dsVersionID>' in ds_profile_text) self.assert_('<dsCreateDate>%s' % self.today in ds_profile_text) self.assert_('<dsState>A</dsState>' in ds_profile_text) self.assert_('<dsMIME>text/xml</dsMIME>' in ds_profile_text) self.assert_('<dsControlGroup>X</dsControlGroup>' in ds_profile_text) self.assert_('<dsVersionable>true</dsVersionable>' in ds_profile_text) # get server datetime param (the hard way) for testing dsprofile_node = etree.fromstring(ds_profile, base_url=r.url) created_s = dsprofile_node.xpath('string(m:dsCreateDate)', namespaces={'m': FEDORA_MANAGE_NS}) created = fedoratime_to_datetime(created_s) # with date param r = self.rest_api.getDatastream(self.pid, "DC", asOfDateTime=created + ONE_SEC) ds_profile_now = r.text # NOTE: contents are not exactly equal because 'now' version includes a dateTime attribute self.assert_('<dsLabel>Dublin Core</dsLabel>' in ds_profile_now) self.assert_('<dsVersionID>DC.0</dsVersionID>' in ds_profile_now) # bogus datastream id on valid pid self.assertRaises(Exception, self.rest_api.getDatastream, self.pid, "BOGUS") # bogus pid self.assertRaises(Exception, self.rest_api.getDatastream, "bogus:pid", "DC") def test_getDatastreamHistory(self): r = self.rest_api.getDatastreamHistory(self.pid, "DC") # default format is html self.assert_('<h3>Datastream History View</h3>' in r.text) r = self.rest_api.getDatastreamHistory(self.pid, "DC", format='xml') data = r.text # check various pieces of datastream info self.assert_('<dsVersionID>DC.0</dsVersionID>' in data) self.assert_('<dsControlGroup>X</dsControlGroup>' in data) self.assert_('<dsLabel>Dublin Core</dsLabel>' in data) self.assert_('<dsVersionable>true</dsVersionable>' in data) self.assert_('<dsMIME>text/xml</dsMIME>' in data) self.assert_('<dsState>A</dsState>' in data) # modify DC so there are multiple versions new_dc = """<oai_dc:dc xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:oai_dc='http://www.openarchives.org/OAI/2.0/oai_dc/'> <dc:title>Test-Object</dc:title> <dc:description>modified!</dc:description> </oai_dc:dc>""" self.rest_api.modifyDatastream(self.pid, "DC", "DCv2Dublin Core", mimeType="text/xml", logMessage="updating DC", content=new_dc) r = self.rest_api.getDatastreamHistory(self.pid, 'DC', format='xml') data = r.text # should include both versions self.assert_('<dsVersionID>DC.0</dsVersionID>' in data) self.assert_('<dsVersionID>DC.1</dsVersionID>' in data) # bogus datastream self.assertRaises(RequestFailed, self.rest_api.getDatastreamHistory, self.pid, "BOGUS") # bogus pid self.assertRaises(RequestFailed, self.rest_api.getDatastreamHistory, "bogus:pid", "DC") def test_getNextPID(self): r = self.rest_api.getNextPID() self.assert_('<pidList' in r.text) self.assert_('<pid>' in r.text) r = self.rest_api.getNextPID(numPIDs=3, namespace="test-ns") self.assertEqual(3, r.text.count("<pid>test-ns:")) def test_getObjectXML(self): # update the object so we can look for audit trail in object xml added, ds = self._add_text_datastream() r = self.rest_api.getObjectXML(self.pid) objxml = r.text self.assert_('<foxml:digitalObject' in objxml) self.assert_('<foxml:datastream ID="DC" ' in objxml) # audit trail accessible in full xml self.assert_('<audit:auditTrail ' in objxml) # bogus id self.assertRaises(Exception, self.rest_api.getObjectXML, "bogus:pid") def test_ingest(self): obj = self.loadFixtureData('basic-object.foxml') r = self.rest_api.ingest(obj) pid = r.content self.assertTrue(pid) self.rest_api.purgeObject(force_text(pid)) # test ingesting with log message response = self.rest_api.ingest(obj, "this is my test ingest message") pid = response.text # ingest message is stored in AUDIT datastream # - can currently only be accessed by retrieving entire object xml r = self.rest_api.getObjectXML(pid) self.assertTrue("this is my test ingest message" in r.text) self.rest_api.purgeObject(pid, "removing test ingest object") def test_ingest_utf8(self): # ingest with unicode log message obj = self.loadFixtureData('basic-object.foxml') response = self.rest_api.ingest(obj, logMessage=self.unicode_test_str) pid = response.text self.assertTrue(pid) response = self.rest_api.getObjectXML(pid) response.encoding = 'utf-8' # ensure requests decodes as utf-8 self.assert_(u'<audit:justification>%s</audit:justification>' % self.unicode_test_str in response.text) self.rest_api.purgeObject(force_text(pid)) # ingest with unicode object label # convert to text to replace string, then convert back to bytes obj = force_bytes(force_text(obj).replace( u"A test object", self.unicode_test_str)) response = self.rest_api.ingest(obj) pid = response.text self.assertTrue(pid) # object label in profile should match the unicode sent response = self.rest_api.getObjectProfile(pid) response.encoding = 'utf-8' # ensure requests decodes as utf-8 self.assert_(u'<objLabel>%s</objLabel>' % self.unicode_test_str in response.text) self.rest_api.purgeObject(force_text(pid)) def test_modifyDatastream(self): # add a datastream to be modified added, ds = self._add_text_datastream() new_text = """Sigh no more, ladies sigh no more. Men were deceivers ever. So be you blythe and bonny, singing hey-nonny-nonny.""" FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write(new_text) FILE.flush() # modify managed datastream by file r = self.rest_api.modifyDatastream(self.pid, ds['id'], "text datastream (modified)", mimeType="text/other", logMessage="modifying TEXT datastream", content=open(FILE.name)) self.assertTrue(r.status_code == requests.codes.ok) # log message in audit trail r = self.rest_api.getObjectXML(self.pid) self.assert_('modifying TEXT datastream' in r.text) r = self.rest_api.getDatastream(self.pid, ds['id']) ds_profile = r.text self.assert_('<dsLabel>text datastream (modified)</dsLabel>' in ds_profile) self.assert_('<dsVersionID>%s.1</dsVersionID>' % ds['id'] in ds_profile) self.assert_('<dsState>A</dsState>' in ds_profile) self.assert_('<dsMIME>text/other</dsMIME>' in ds_profile) r = self.rest_api.getDatastreamDissemination(self.pid, ds['id']) self.assertEqual(r.text, new_text) # modify DC (inline xml) by string new_dc = """<oai_dc:dc xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:oai_dc='http://www.openarchives.org/OAI/2.0/oai_dc/'> <dc:title>Test-Object</dc:title> <dc:description>modified!</dc:description> </oai_dc:dc>""" response = self.rest_api.modifyDatastream(self.pid, "DC", "Dublin Core", mimeType="text/xml", logMessage="updating DC", content=new_dc) self.assertTrue(response.status_code == requests.codes.ok) response = self.rest_api.getDatastreamDissemination(self.pid, "DC") # fedora changes whitespace in xml, so exact test fails dc = response.text self.assert_('<dc:title>Test-Object</dc:title>' in dc) self.assert_('<dc:description>modified!</dc:description>' in dc) # invalid checksum self.assertRaises( ChecksumMismatch, self.rest_api.modifyDatastream, self.pid, "DC", "Dublin Core", mimeType="text/xml", logMessage="updating DC", content=new_dc, checksum='totally-bogus-not-even-an-MD5', checksumType='MD5') # bogus datastream on valid pid self.assertRaises( RequestFailed, self.rest_api.modifyDatastream, self.pid, "BOGUS", "Text DS", mimeType="text/plain", logMessage="modifiying non-existent DS", content=open(FILE.name)) # bogus pid self.assertRaises(RequestFailed, self.rest_api.modifyDatastream, "bogus:pid", "TEXT", "Text DS", mimeType="text/plain", logMessage="modifiying non-existent DS", content=open(FILE.name)) FILE.close() def test_modifyDatastream_utf8(self): # unicode in datastream label or log message should not cause errors # - unicode doesn't seem to be a problem here, perhaps because it is # a PUT rather than a POST added, ds = self._add_text_datastream() # modify managed datastream by file response = self.rest_api.modifyDatastream( self.pid, ds['id'], self.unicode_test_str, mimeType="text/plain", logMessage="modifying TEXT datastream") self.assertTrue(response.status_code == requests.codes.ok) # confirm unicode label is listed correctly in datastream profile response = self.rest_api.getDatastream(self.pid, ds['id']) response.encoding = 'utf-8' # ensure requests decodes as utf-8 self.assert_(six.u('<dsLabel>%s</dsLabel>') % self.unicode_test_str in response.text) # unicode in log message response = self.rest_api.modifyDatastream( self.pid, ds['id'], 'text content', mimeType="text/plain", logMessage=self.unicode_test_str) self.assertTrue(response.status_code == requests.codes.ok) # log message should be in audit trail response = self.rest_api.getObjectXML(self.pid) response.encoding = 'utf-8' self.assert_(six.u('<audit:justification>%s</audit:justification>') % self.unicode_test_str in response.text) def test_modifyObject(self): response = self.rest_api.modifyObject( self.pid, "modified test object", "testuser", "I", "testing modify object") modified = (response.status_code == requests.codes.ok) self.assertTrue(modified) # log message in audit trail response = self.rest_api.getObjectXML(self.pid) self.assert_('testing modify object' in response.text) response = self.rest_api.getObjectProfile(self.pid) profile = response.text self.assert_('<objLabel>modified test object</objLabel>' in profile) self.assert_('<objOwnerId>testuser</objOwnerId>' in profile) self.assert_('<objState>I</objState>' in profile) # bogus id self.assertRaises( RequestFailed, self.rest_api.modifyObject, "bogus:pid", "modified test object", "testuser", "I", "testing modify object") def test_modifyObject_utf8(self): # unicode doesn't seem to be a problem here, perhaps because it is # a PUT rather than a POST response = self.rest_api.modifyObject( self.pid, self.unicode_test_str, "testuser", "I", "testing modify object with unicode label") # saving unicode label shouldn't error self.assertEqual(response.status_code, requests.codes.ok) # object label in profile should match the unicode sent response = self.rest_api.getObjectProfile(self.pid) response.encoding = 'utf-8' self.assert_(six.u('<objLabel>%s</objLabel>') % self.unicode_test_str in response.text) # saving with unicode log message shouln't error response = self.rest_api.modifyObject( self.pid, "modified test object", "testuser", "A", self.unicode_test_str) self.assertEqual(response.status_code, requests.codes.ok) response = self.rest_api.getObjectXML(self.pid) response.encoding = 'utf-8' self.assert_(six.u('<audit:justification>%s</audit:justification>') % self.unicode_test_str in response.text) def test_purgeDatastream(self): # add a datastream that can be purged (added, dsprofile), ds = self._add_text_datastream() # grab datastream creation date from addDatastream result to test purge result dsprofile_node = etree.fromstring(dsprofile) created = dsprofile_node.xpath('string(m:dsCreateDate)', namespaces={'m': FEDORA_MANAGE_NS}) # purgeDatastream gives us back the time in a different format: expect_created = created if expect_created.endswith('Z'): # it does # strip of the Z and any final zeros expect_created = expect_created.rstrip('Z0') # strip the decimal if it got that far expect_created = expect_created.rstrip('.') # and put back the Z expect_created += 'Z' r = self.rest_api.purgeDatastream(self.pid, ds['id'], logMessage="purging text datastream") purged = (r.status_code == requests.codes.ok) times = r.text self.assertTrue(purged) self.assert_(expect_created in times, 'datastream creation date should be returned in list of purged datastreams - expected %s, got %s' % \ (expect_created, times)) # log message in audit trail r = self.rest_api.getObjectXML(self.pid) self.assert_('purging text datastream' in r.text) # datastream no longer listed r = self.rest_api.listDatastreams(self.pid) self.assert_('<datastream dsid="%s"' % ds['id'] not in r.text) # NOTE: Fedora bug - attempting to purge a non-existent datastream returns 204? # purged = self.rest_api.purgeDatastream(self.pid, "BOGUS", # logMessage="test purging non-existent datastream") # self.assertFalse(purged) self.assertRaises(RequestFailed, self.rest_api.purgeDatastream, "bogus:pid", "BOGUS", logMessage="test purging non-existent datastream from non-existent object") # also test purging specific versions of a datastream ? # attempt to purge a version that doesn't exist (added, dsprofile), ds = self._add_text_datastream() tomorrow = datetime.now(tzutc()) + timedelta(1) r = self.rest_api.purgeDatastream(self.pid, ds['id'], startDT=datetime_to_fedoratime(tomorrow), logMessage="purging text datastream") success = (r.status_code == requests.codes.ok) times = r.text # no errors, no versions purged self.assertTrue(success) self.assertEqual('[]', times) def test_purgeObject(self): obj = load_fixture_data('basic-object.foxml') r = self.rest_api.ingest(obj) pid = r.text r = self.rest_api.purgeObject(pid) purged = (r.status_code == requests.codes.ok) self.assertTrue(purged) # NOTE: fedora doesn't notice the object has been purged right away sleep(7) # 5-6 was fastest this worked; padding to avoid spurious failures self.assertRaises(Exception, self.rest_api.getObjectProfile, pid) # bad pid self.assertRaises(RequestFailed, self.rest_api.purgeObject, "bogus:pid") def test_purgeRelationship(self): # add relation to purg self.rest_api.addRelationship(self.pid, 'info:fedora/%s' % self.pid, predicate=force_text(modelns.hasModel), object='info:fedora/pid:123') purged = self.rest_api.purgeRelationship(self.pid, 'info:fedora/%s' % self.pid, force_text(modelns.hasModel), 'info:fedora/pid:123') self.assertEqual(purged, True) # purge non-existent rel on valid pid purged = self.rest_api.purgeRelationship(self.pid, 'info:fedora/%s' % self.pid, self.rel_owner, 'johndoe', isLiteral=True) self.assertFalse(purged) # bogus pid self.assertRaises(RequestFailed, self.rest_api.purgeRelationship, "bogus:pid", 'info:fedora/bogus:pid', self.rel_owner, "johndoe", True) def test_setDatastreamState(self): # in Fedora 3.5, Fedora returns a BadRequest when we attempt to # mark DC as inactive (probably reasonable); testing on a # non-required datastream instead. (added, dsprofile), ds = self._add_text_datastream() set_state = self.rest_api.setDatastreamState(self.pid, "TEXT", "I") self.assertTrue(set_state) # get datastream to confirm change r = self.rest_api.getDatastream(self.pid, "TEXT") self.assert_('<dsState>I</dsState>' in r.text) # bad datastream id self.assertRaises(RequestFailed, self.rest_api.setDatastreamState, self.pid, "BOGUS", "I") # non-existent pid self.assertRaises(RequestFailed, self.rest_api.setDatastreamState, "bogus:pid", "DC", "D") def test_setDatastreamVersionable(self): # In Fedora 3.5, Fedora returns a BadRequest when we attempt # to change DC versionable (reasonable?); testing on a # non-required datastream instead. (added, dsprofile), ds = self._add_text_datastream() set_versioned = self.rest_api.setDatastreamVersionable(self.pid, "TEXT", False) self.assertTrue(set_versioned) # get datastream profile to confirm change r = self.rest_api.getDatastream(self.pid, "TEXT") self.assert_('<dsVersionable>false</dsVersionable>' in r.text) # bad datastream id self.assertRaises(RequestFailed, self.rest_api.setDatastreamVersionable, self.pid, "BOGUS", False) # non-existent pid self.assertRaises(RequestFailed, self.rest_api.setDatastreamVersionable, "bogus:pid", "DC", True) # utility methods def test_upload_string(self): data = "Here is some temporary content to upload to fedora." upload_id = self.rest_api.upload(data) # current format looks like uploaded://#### pattern = re.compile('uploaded://[0-9]+') self.assert_(pattern.match(force_text(upload_id))) def test_upload_file(self): FILE = tempfile.NamedTemporaryFile(mode="w", suffix=".txt") FILE.write("Here is some temporary content to upload to fedora.") FILE.flush() with open(FILE.name, 'rb') as f: upload_id = self.rest_api.upload(f) # current format looks like uploaded://#### pattern = re.compile('uploaded://[0-9]+') self.assert_(pattern.match(upload_id)) self.assertTrue(pattern.match(upload_id)) def test_upload_generator(self): # test uploading content from a generator def data_generator(): yield 'line one of text\n' yield 'line two of text\n' yield 'line three of text\n' text_content = ''.join(data_generator()) content_md5 = md5sum(text_content) size = len(text_content) upload_id = self.rest_api.upload(data_generator(), size=size, content_type='text/plain') pattern = re.compile('uploaded://[0-9]+') self.assertTrue(pattern.match(upload_id)) # check that the *right* content was uploaded by adding # a datastream using the computed MD5 and generated upload id obj = load_fixture_data('basic-object.foxml') response = self.rest_api.ingest(obj) pid = response.text add_response = self.rest_api.addDatastream(pid, 'text', controlGroup='M', dsLocation=upload_id, mimeType='text/plain', checksumType='MD5', checksum=content_md5) self.assertTrue(add_response.status_code, requests.codes.created) # get the content from fedora and confirm it matches what was sent dsresponse = self.rest_api.getDatastreamDissemination(pid, 'text') self.assertEqual(text_content, dsresponse.text) # clean up test object self.rest_api.purgeObject(pid) def test_retries(self): with patch('eulfedora.api.requests.adapters') as mockreq_adapters: # retries not specified, retries = None REST_API(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD) # no custom adapter code needed mockreq_adapters.HTTPAdapter.assert_not_called() # retry value specified REST_API(FEDORA_ROOT_NONSSL, FEDORA_USER, FEDORA_PASSWORD, retries=3) # adapter should be initialized with max retries option mockreq_adapters.HTTPAdapter.assert_called_with(max_retries=3)