예제 #1
0
 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()
예제 #2
0
    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)
예제 #3
0
 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()
예제 #4
0
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)
예제 #5
0
 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()
예제 #6
0
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))
예제 #7
0
 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()
예제 #8
0
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)