From 36f8564dfe8ff8f23c16290cd4eab4f3d373a5df Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Mon, 27 May 2019 11:16:54 -0500 Subject: [PATCH 01/20] Re-add in camel-toolbox test --- README.md | 42 ++++++++++---- TestConstants.py | 12 ++++ camel_tests.py | 136 +++++++++++++++++++++++++++++++++++++++++++++ config.yml.example | 10 ++++ rdf_tests.py | 2 +- testrunner.py | 12 +++- 6 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 camel_tests.py diff --git a/README.md b/README.md index 27835f8..c0d782b 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ second_site: password1: password1 user2: user2 password2: password2 + solrurl: http://someserver:8080/solr + triplestoreurl: http://someserver:8080/fuseki/test/sparql ``` To use the `second_site` configuration, simply start the testrunner with @@ -82,11 +84,13 @@ You can also choose to run only a subset of all tests using the `-t|--tests` arg of the following values which indicate which tests to run. * `authz` - Authorization tests * `basic` - Basic interaction tests -* `sparql` - Sparql operation tests +* `camel` - Camel toolbox tests (see [note](#camel-tests)) +* `fixity` - Binary fixity tests +* `indirect` - Indirect container tests * `rdf` - RDF serialization tests -* `version` - Versioning tests +* `sparql` - Sparql operation tests * `transaction` - Transcation tests -* `fixity` - Binary fixity tests +* `version` - Versioning tests Without this parameter all the above tests will be run. @@ -95,6 +99,14 @@ To run only the `authz` and `sparql` tests you would execute: ./testrunner.py -c config.yml -t authz,sparql ``` +##### Camel Tests +`camel` tests are **NOT** executed by default, due to timing issues they should be run separately. + +They also require the configuration to have a `solrurl` parameter pointing to a Solr endpoint and a +`triplestoreurl` parameter pointing to the SPARQL endpoint of a triplestore fed. + +Both of these systems must be fed by the fcrepo-camel-toolbox for this testing. + ## Tests implemented ### authz @@ -121,11 +133,27 @@ To run only the `authz` and `sparql` tests you would execute: 1. Create a LDP Indirect container 1. Validate the correct Link header type +### camel - not run by default +1. Create a container +1. Check the container is indexed to Solr +1. Check the container is indexed to the triplestore + ### fixity 1. Create a binary resource 1. Get a fixity result for that resource 1. Compare that the SHA-1 hash matches the expected value +### indirect +1. Create a pcdm:Object +2. Create a pcdm:Collection +3. Create an indirect container "members" inside the pcdm:Collection +4. Create a proxy object for the pcdm:Object inside the **members** indirectContainer +5. Verify that the pcdm:Collection has the memberRelation property added pointing to the pcdm:Object + +### rdf +1. Create a RDFSource object. +1. Retrieve that object in all possible RDF serializations. + ### sparql 1. Create a container 1. Set the dc:title of the container with a Patch request @@ -137,7 +165,7 @@ To run only the `authz` and `sparql` tests you would execute: 1. Verify the title 1. Create a container 1. Update the title to text with Unicode characters -1. Verify the title +1. Verify the title ### transaction 1. Create a transaction @@ -170,9 +198,3 @@ To run only the `authz` and `sparql` tests you would execute: 1. Create Memento at deleted memento's datetime 1. Verify Memento exists -### indirect -1. Create a pcdm:Object -2. Create a pcdm:Collection -3. Create an indirect container "members" inside the pcdm:Collection -4. Create a proxy object for the pcdm:Object inside the **members** indirectContainer -5. Verify that the pcdm:Collection has the memberRelation property added pointing to the pcdm:Object diff --git a/TestConstants.py b/TestConstants.py index 7cee1b8..fa66611 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -10,6 +10,8 @@ USER2_PASS_PARAM = "password2" LOG_FILE_PARAM = "logfile" SELECTED_TESTS_PARAM = "selected_tests" +SOLR_URL_PARAM = "solrurl" +TRIPLESTORE_URL_PARAM = "triplestoreurl" # Via RFC 7231 3.3 PAYLOAD_HEADERS = ['Content-Length', 'Content-Range', 'Trailer', 'Transfer-Encoding'] @@ -31,6 +33,9 @@ # General Mime and LDP constants JSONLD_MIMETYPE = "application/ld+json" SPARQL_UPDATE_MIMETYPE = "application/sparql-update" +TURTLE_MIMETYPE = "text/turtle" +SPARQL_QUERY_MIMETYPE = "application/sparql-query" +SPARQL_RESULT_JSON_MIMETYPE = "application/sparql-results+json" LDP_NS = "http://www.w3.org/ns/ldp#" LDP_CONTAINER = LDP_NS + "Container" @@ -50,3 +55,10 @@ "@prefix pcdm: ." \ "<> a pcdm:Object ;" \ "dc:title \"An Object\" ." + +PCDM_CONTAINER_TITLE = "PCDM Container" + +PCDM_CONTAINER_TTL = "@prefix dc: ." \ + "@prefix pcdm: ." \ + "<> a pcdm:Object ;" \ + "dc:title \"{0}\" .".format(PCDM_CONTAINER_TITLE) diff --git a/camel_tests.py b/camel_tests.py new file mode 100644 index 0000000..c7ab24f --- /dev/null +++ b/camel_tests.py @@ -0,0 +1,136 @@ +#!/bin/env python + +import TestConstants +from abstract_fedora_tests import FedoraTests, register_tests, Test +import uuid +import datetime +import requests +import json +import pyjq + + +@register_tests +class FedoraCamelTests(FedoraTests): + + # Create test objects all inside here for easy of review. + CONTAINER = "/test_camel" + + # How many seconds to wait for indexing to Solr and/or triplestore. + CONTAINER_WAIT = 30 + + def run_tests(self): + if not (self.hasSolrUrl() and self.hasTriplestoreUrl()): + print("**** Cannot run camel tests without a Solr and/or Triplestore base url ****") + else: + super().run_tests() + + @Test + def createObject(self): + self.log("Create an object") + internal_id = str(uuid.uuid4()) + expected_url = self.getBaseUri() + "/" + internal_id + headers = { + 'Slug': internal_id, + 'Content-type': TestConstants.TURTLE_MIMETYPE + } + r = self.do_post(headers=headers, body=TestConstants.PCDM_CONTAINER_TTL) + self.assertEqual(201, r.status_code, "Did not get expected response code") + + if self.hasSolrUrl(): + self.log("Checking for item in Solr") + solr_response = self.timedQuery('querySolr', 'solrNumFound', expected_url) + if solr_response is not None: + self.assertEqual(expected_url, self.getIdFromSolr(solr_response), "Did not find ID in Solr.") + else: + self.fail("Timed out waiting to find record in Solr.") + + if self.hasTriplestoreUrl(): + self.log("Checking for item in Triplestore") + triplestore_response = self.timedQuery('queryTriplestore', 'triplestoreNumFound', expected_url) + if triplestore_response is not None: + self.assertEqual(TestConstants.PCDM_CONTAINER_TITLE, self.getIdFromTriplestore(triplestore_response), + "Did not find ID in Triplestore") + else: + self.fail("Timed out waiting to find record in Triplestore") + + # Utility functions. + def timedQuery(self, query_method, check_method, query_value): + query_func = getattr(self, query_method, None) + check_func = getattr(self, check_method, None) + if query_func is None or check_func is None: + Exception("Can't find expected methods {0}, {1}".format(query_method, check_method)) + current_time = datetime.datetime.now() + end_time = current_time + datetime.timedelta(seconds=self.CONTAINER_WAIT) + last_query = None + while current_time <= end_time: + # If a multiple of 5 seconds has passed since the last query, do the query + if last_query is None or (current_time - last_query).seconds >= 5: + last_query = datetime.datetime.now() + response = query_func(query_value) + if check_func(response) is not None: + return response + current_time = datetime.datetime.now() + return None + + def hasSolrUrl(self): + try: + return self.config[TestConstants.SOLR_URL_PARAM] is not None and \ + len(self.config[TestConstants.SOLR_URL_PARAM].strip()) > 0 + except KeyError: + return False + + def hasTriplestoreUrl(self): + try: + return self.config[TestConstants.TRIPLESTORE_URL_PARAM] is not None and \ + len(self.config[TestConstants.TRIPLESTORE_URL_PARAM].strip()) > 0 + except KeyError: + return False + + def solrNumFound(self, response): + body = response.content.decode('UTF-8') + json_body = json.loads(body) + num_found = pyjq.first('.response.numFound', json_body) + if num_found is not None and int(num_found) > 0: + return num_found + return None + + def querySolr(self, expected_id): + solr_select = self.config[TestConstants.SOLR_URL_PARAM].rstrip('/') + "/select" + params = { + 'q': 'id:"' + expected_id + '"', + 'wt': 'json' + } + r = requests.get(solr_select, params=params) + self.assertEqual(200, r.status_code, "Did not query Solr successfully.") + return r + + def getIdFromSolr(self, response): + body = response.content.decode('UTF-8') + json_body = json.loads(body) + found_title = pyjq.first('.response.docs[].id', json_body) + return found_title + + def queryTriplestore(self, expected_id): + query = "PREFIX dc: SELECT ?o WHERE { <" + expected_id + "> dc:title ?o}" + headers = { + 'Content-type': TestConstants.SPARQL_QUERY_MIMETYPE, + 'Accept': TestConstants.SPARQL_RESULT_JSON_MIMETYPE + } + r = requests.post(self.config[TestConstants.TRIPLESTORE_URL_PARAM], headers=headers, data=query) + self.assertEqual(200, r.status_code, 'Did not query Triplestore successfully.') + return r + + def triplestoreNumFound(self, response): + body = response.content.decode('UTF-8') + json_body = json.loads(body) + # This results a list of matching bindings, so it can be an empty list. + num_found = pyjq.all('.results.bindings[].o', json_body) + if num_found is not None and len(num_found) > 0: + return num_found + return None + + def getIdFromTriplestore(self, response): + body = response.content.decode('UTF-8') + json_body = json.loads(body) + found_id = pyjq.first('.results.bindings[].o.value', json_body) + return found_id diff --git a/config.yml.example b/config.yml.example index dc7b0fc..7be2a77 100644 --- a/config.yml.example +++ b/config.yml.example @@ -7,3 +7,13 @@ default: password1: testpass user2: testuser2 password2: testpass +tomcat: + baseurl: http://localhost:8080/fcrepo/rest + admin_user: fedoraAdmin + admin_password: secret3 + user1: adminuser + password1: password2 + user2: testuser + password2: password1 + solrurl: http://localhost:8080/solr + triplestoreurl: http://localhost:8080/fuseki/test/query diff --git a/rdf_tests.py b/rdf_tests.py index dc65436..85cf9b1 100644 --- a/rdf_tests.py +++ b/rdf_tests.py @@ -64,4 +64,4 @@ def testRdfSerialization(self): r = self.do_get(location) self.assertEqual(410, r.status_code, "Object's tombstone not found.") - self.log("Passed") \ No newline at end of file + self.log("Passed") diff --git a/testrunner.py b/testrunner.py index f7baf14..aa19dc6 100755 --- a/testrunner.py +++ b/testrunner.py @@ -17,6 +17,7 @@ from transaction_tests import FedoraTransactionTests from authz_tests import FedoraAuthzTests from indirect_tests import FedoraIndirectTests +from camel_tests import FedoraCamelTests class FedoraTestRunner: @@ -31,7 +32,9 @@ class FedoraTestRunner: (TestConstants.USER2_NAME_PARAM, True), (TestConstants.USER2_PASS_PARAM, True), (TestConstants.LOG_FILE_PARAM, False), - (TestConstants.SELECTED_TESTS_PARAM, False) + (TestConstants.SELECTED_TESTS_PARAM, False), + (TestConstants.SOLR_URL_PARAM, False), + (TestConstants.TRIPLESTORE_URL_PARAM, False) ] config = {} logger = None @@ -99,6 +102,9 @@ def run_tests(self): if test == 'all' or test == 'indirect': indirect = FedoraIndirectTests(self.config) indirect.run_tests() + if test == 'camel': + camel = FedoraCamelTests(self.config) + camel.run_tests() def main(self, args): self.set_up(args) @@ -116,7 +122,7 @@ def csv_list(string): class CSVAction(argparse.Action): - valid_options = ["authz", "basic", "sparql", "rdf", "version", "transaction", "fixity", "indirect"] + valid_options = ["authz", "basic", "sparql", "rdf", "version", "transaction", "fixity", "indirect", "camel"] def __call__(self, parser, args, values, option_string=None): if isinstance(values, list): @@ -158,7 +164,7 @@ def __call__(self, parser, args, values, option_string=None): parser.add_argument('-k', '--' + TestConstants.USER2_PASS_PARAM, dest=TestConstants.USER2_PASS_PARAM, help="Second regular user password") parser.add_argument('-t', '--tests', dest="selected_tests", help='Comma separated list of which tests to run from ' - '{0}. Defaults to running all tests'.format(",".join(CSVAction.valid_options)), + '{0}. Defaults to running all tests'.format(", ".join(CSVAction.valid_options)), default=['all'], type=csv_list, action=CSVAction) args = parser.parse_args() From de73330eab49fc87412c11da5fbd7d409fc80aa3 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Mon, 27 May 2019 11:58:27 -0500 Subject: [PATCH 02/20] Fix readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c0d782b..5290871 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ To run only the `authz` and `sparql` tests you would execute: `camel` tests are **NOT** executed by default, due to timing issues they should be run separately. They also require the configuration to have a `solrurl` parameter pointing to a Solr endpoint and a -`triplestoreurl` parameter pointing to the SPARQL endpoint of a triplestore fed. +`triplestoreurl` parameter pointing to the SPARQL endpoint of a triplestore. Both of these systems must be fed by the fcrepo-camel-toolbox for this testing. @@ -133,7 +133,7 @@ Both of these systems must be fed by the fcrepo-camel-toolbox for this testing. 1. Create a LDP Indirect container 1. Validate the correct Link header type -### camel - not run by default +### camel - see [note](#camel-tests) 1. Create a container 1. Check the container is indexed to Solr 1. Check the container is indexed to the triplestore From 5681a6d869a881c56efdf4a0bcfd58b4232b27f2 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Tue, 4 Jun 2019 11:22:49 -0500 Subject: [PATCH 03/20] Some refactoring to separate messages from tests and classes --- abstract_fedora_tests.py | 43 ++++++++++++++++++++--------- authz_tests.py | 55 ++++++++++++++++++++++++++++++-------- basic_interaction_tests.py | 22 --------------- camel_tests.py | 2 +- fixity_tests.py | 2 -- indirect_tests.py | 4 --- rdf_tests.py | 4 --- sparql_tests.py | 12 --------- transaction_tests.py | 8 ------ version_tests.py | 8 ------ 10 files changed, 75 insertions(+), 85 deletions(-) diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index 1e500c2..83aa5c3 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -33,14 +33,23 @@ def getFedoraBase(self): """ Return the Fedora Base URI """ return self.config[TestConstants.BASE_URL_PARAM] + @staticmethod + def getCurrentClass(): + return inspect.stack()[1][0].f_locals['self'].__class__.__name__ + def run_tests(self): + current = FedoraTests.getCurrentClass() self.check_for_retest(self.getBaseUri()) + self.log("\nStarting class {0}\n".format(current)) """ Check we can access the machine and then run the tests """ self.not_authorized() for test in self._testdict: method = getattr(self, test) + self.log("Running {0}".format(test)) method() + self.log("Passed\n") self.cleanup(self.getBaseUri()) + self.log("\nExiting class {0}".format(current)) def not_authorized(self): """ Ensure we can even access the repository """ @@ -181,21 +190,29 @@ def cleanup(self, uri): def check_for_retest(self, uri): """Try to create CONTAINER """ - response = self.do_put(uri) - if response.status_code != 201: - caller = inspect.stack()[1][0].f_locals['self'].__class__.__name__ - print("The class ({}) has been run.\nYou need to remove the resource ({}) and all it's " - "children before re-running the test.".format(caller, uri)) - rerun = input("Remove the test objects and re-run? (y/N) ") - if rerun.lower().strip() == 'y': - if self.cleanup(uri): - self.do_put(uri) + try: + response = self.do_put(uri) + if response.status_code == 403: + print("Received a 403 Forbidden response, please check your credentials and try again.") + quit() + elif response.status_code != 201: + caller = FedoraTests.getCurrentClass() + print("The class ({0}) has been run.\nYou need to remove the resource ({1}) and all it's " + "children before re-running the test.".format(caller, uri)) + rerun = input("Remove the test objects and re-run? (y/N) ") + if rerun.lower().strip() == 'y': + if self.cleanup(uri): + self.do_put(uri) + else: + print("Error removing {0}, you may need to remove it manually.".format(uri)) + quit() else: - print("Error removing $URL, you may need to remove it manually.") + print("Exiting...") quit() - else: - print("Exiting...") - quit() + except requests.exceptions.ConnectionError: + self.log("Unable to connect to your repository, if you are sure its running. Please check your base uri " + "in the configuration.") + quit() @staticmethod def get_link_headers(response): diff --git a/authz_tests.py b/authz_tests.py index acf40ca..1da9ac8 100644 --- a/authz_tests.py +++ b/authz_tests.py @@ -37,20 +37,18 @@ def verifyAuthEnabled(self): self.assertEqual(401, r.status_code, "Did not get expected response code") def getAclUri(self, response): - acls = self.get_link_headers(response) + acls = FedoraAuthzTests.get_link_headers(response) self.assertIsNotNone(acls['acl']) return acls['acl'][0] @Test def doAuthTests(self): - self.log("Running doAuthTests") - self.verifyAuthEnabled() self.log("Create \"cover\" container") r = self.do_put(self.getBaseUri() + "/cover") self.assertEqual(201, r.status_code, "Did not get expected response code") - cover_location = self.get_location(r) + cover_location = FedoraAuthzTests.get_location(r) cover_acl = self.getAclUri(r) self.log("Make \"cover\" a pcdm:Object") @@ -78,7 +76,7 @@ def doAuthTests(self): self.log("Create \"files\" inside \"cover\"") r = self.do_put(cover_location + "/files") self.assertEqual(201, r.status_code, "Did not get expected response code") - files_location = self.get_location(r) + files_location = FedoraAuthzTests.get_location(r) files_acl = self.getAclUri(r) self.log("Anonymous can't access \"cover\"") @@ -135,24 +133,20 @@ def doAuthTests(self): r = self.do_get(files_location, admin=auth) self.assertEqual(200, r.status_code, "Did not get expected response code") - self.log("Passed") - @Test def doDirectIndirectAuthTests(self): - self.log("Running doDirectIndirectAuthTests") - self.verifyAuthEnabled() self.log("Create a target container") r = self.do_post() self.assertEqual(201, r.status_code, "Did not get expected response code") - target_location = self.get_location(r) + target_location = FedoraAuthzTests.get_location(r) target_acl = self.getAclUri(r) self.log("Create a write container") r = self.do_post() self.assertEqual(201, r.status_code, "Did not get expected response code") - write_location = self.get_location(r) + write_location = FedoraAuthzTests.get_location(r) write_acl = self.getAclUri(r) self.log("Make sure the /target resource is readonly") @@ -239,3 +233,42 @@ def doDirectIndirectAuthTests(self): self.config[TestConstants.USER_NAME_PARAM])) r = self.do_put(write_location + "/" + uuid_value, admin=False) self.assertEqual(201, r.status_code, "Did not get expected response code") + + @Test + def multipleAuthzCreatePermissiveSet(self): + self.verifyAuthEnabled() + + self.log("Create a target container") + r = self.do_post() + self.assertEqual(201, r.status_code, "Did not get expected response code") + target_location = FedoraAuthzTests.get_location(r) + target_acl = self.getAclUri(r) + + double_ttl = "@prefix acl: .\n" \ + "<#readonly> a acl:Authorization ;\n" \ + " acl:agent \"{0}\" ;\n" \ + " acl:mode acl:Read ;\n" \ + " acl:accessTo <{1}> .\n" \ + "<#readwrite> a acl:Authorization ;\n" \ + " acl:agent \"{0}\" ;\n" \ + " acl:mode acl:Write ;\n" \ + " acl:accessTo <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], target_location) + headers = { + 'Content-type': TestConstants.TURTLE_MIMETYPE + } + + self.log("Add ACL with one read and one read/write authz.") + r = self.do_put(target_acl, headers=headers, body=double_ttl) + self.assertEqual(201, r.status_code, "Did not get expected response code") + + self.log("Check we can read") + r = self.do_get(target_location, admin=False) + self.assertEqual(200, r.status_code, "Did not get expected response code") + + self.log("Check we can write") + headers = { + 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + } + body = "prefix dc: INSERT { <> dc:title \"A new title\" } WHERE {}" + r = self.do_patch(target_location, headers=headers, body=body) + self.assertEqual(204, r.status_code, "Did not get expected response code") diff --git a/basic_interaction_tests.py b/basic_interaction_tests.py index 4c270dc..f0c89c3 100644 --- a/basic_interaction_tests.py +++ b/basic_interaction_tests.py @@ -31,59 +31,43 @@ def createTestResource(self, type, files=None): @Test def testBasicContainer(self): - self.log("Running testBasicContainer") self.createTestResource(TestConstants.LDP_BASIC) - self.log("Passed") @Test def testDirectContainer(self): - self.log("Running testDirectContainer") self.createTestResource(TestConstants.LDP_DIRECT) - self.log("Passed") @Test def testIndirectContainer(self): - self.log("Running testIndirectContainer") self.createTestResource(TestConstants.LDP_INDIRECT) - self.log("Passed") @Test def testNonRdfSource(self): - self.log("Running testNonRdfSource") testfiles = {'files': ('testdata.csv', 'this,is,some,data\n')} self.createTestResource(TestConstants.LDP_NON_RDF_SOURCE, files=testfiles) - self.log("Passed") @Test def testLdpResource(self): """ We don't allow you to create a ldp:Resource so this returns 400 Bad Request """ - self.log("Running testLdpResource") link_type = self.make_type(TestConstants.LDP_RESOURCE) headers = { 'Link': link_type } r = self.do_post(self.getBaseUri(), headers=headers) self.assertEqual(400, r.status_code, "Did not get expected response") - self.log("Passed") @Test def testLdpContainer(self): """ We don't allow you to create a ldp:Container so this returns 400 Bad Request """ - self.log("Running testLdpResource") link_type = self.make_type(TestConstants.LDP_CONTAINER) headers = { 'Link': link_type } r = self.do_post(self.getBaseUri(), headers=headers) self.assertEqual(400, r.status_code, "Did create container") - self.log("Passed") - - @Test def doNestedTests(self): - self.log("Running doNestedTests") - self.log("Create a container") headers = { 'Content-type': 'text/turtle' @@ -125,8 +109,6 @@ def doNestedTests(self): r = self.do_get(location) self.assertEqual(410, r.status_code, "Did not get expected status code") - self.log("Passed") - def changeIxnModels(self, location, starting_model): """ This function uses a created object at {location} with starting type {starting_model}. The below dictionary of tuples works as such @@ -179,12 +161,8 @@ def changeIxnModels(self, location, starting_model): r = self.do_put(location, headers=headers, files=files) self.assertEqual(result, r.status_code, "Did not get expected response") - self.log("Passed") - @Test def testChangeIxnModel(self): - self.log("Running changeIxnModel") - self.log("Create a basic container") basic = self.createTestResource(TestConstants.LDP_BASIC) self.changeIxnModels(basic, TestConstants.LDP_BASIC) diff --git a/camel_tests.py b/camel_tests.py index c7ab24f..d85716b 100644 --- a/camel_tests.py +++ b/camel_tests.py @@ -25,7 +25,7 @@ def run_tests(self): super().run_tests() @Test - def createObject(self): + def CamelCreateObject(self): self.log("Create an object") internal_id = str(uuid.uuid4()) expected_url = self.getBaseUri() + "/" + internal_id diff --git a/fixity_tests.py b/fixity_tests.py index 30d4244..4f6e9ed 100644 --- a/fixity_tests.py +++ b/fixity_tests.py @@ -43,5 +43,3 @@ def aFixityTest(self): '."http://www.loc.gov/premis/rdf/v1#hasMessageDigest" | .[]?."@id"'.format(fixity_id), json_body) self.assertEqual(self.FIXITY_RESULT, fixity_result, "Fixity result was not a match for expected.") - - self.log("Passed") \ No newline at end of file diff --git a/indirect_tests.py b/indirect_tests.py index bd9fcea..0389327 100644 --- a/indirect_tests.py +++ b/indirect_tests.py @@ -14,8 +14,6 @@ class FedoraIndirectTests(FedoraTests): @Test def doPcdmIndirect(self): - self.log("Running doPcdmIndirect") - self.log("Create a PCDM container") basic_headers = { 'Link': self.make_type(TestConstants.LDP_BASIC), @@ -70,5 +68,3 @@ def doPcdmIndirect(self): if member == pcdm_container_location: found_member = True self.assertTrue(found_member, "Did not find hasMember property") - - self.log("Passed") \ No newline at end of file diff --git a/rdf_tests.py b/rdf_tests.py index 85cf9b1..3e9959d 100644 --- a/rdf_tests.py +++ b/rdf_tests.py @@ -20,8 +20,6 @@ class FedoraRdfTests(FedoraTests): @Test def testRdfSerialization(self): - self.log("Starting testRdfSerialization") - self.log("Put new resource.") headers = { "Content-type": "text/turtle" @@ -63,5 +61,3 @@ def testRdfSerialization(self): self.log("Test for tombstone") r = self.do_get(location) self.assertEqual(410, r.status_code, "Object's tombstone not found.") - - self.log("Passed") diff --git a/sparql_tests.py b/sparql_tests.py index 4a0878c..35d297e 100644 --- a/sparql_tests.py +++ b/sparql_tests.py @@ -19,8 +19,6 @@ class FedoraSparqlTests(FedoraTests): @Test def doSparqlContainerTest(self): - self.log("Running doSparqlContainerTest") - self.log("Create container") headers = { 'Content-type': 'text/turtle' @@ -42,12 +40,8 @@ def doSparqlContainerTest(self): self.assertEqual(204, r.status_code, "Did not get expected results") self.assertTitleExists("Updated title", location) - self.log("Passed") - @Test def doSparqlBinaryTest(self): - self.log("Running doSparqlBinaryTest") - self.log("Create a binary") headers = { 'Content-type': 'image/jpeg' @@ -71,12 +65,8 @@ def doSparqlBinaryTest(self): self.assertEqual(204, r.status_code, "Did not get expected results") self.assertTitleExists("Updated title", description) - self.log("Passed") - @Test def doUnicodeSparql(self): - self.log("Running doUnicodeSparql") - self.log("Create a container") r = self.do_post(self.getBaseUri()) self.assertEqual(201, r.status_code, "Did not get expected response code") @@ -93,5 +83,3 @@ def doUnicodeSparql(self): r = self.do_patch(location, headers=headers, body=sparql) self.assertEqual(204, r.status_code, "Did not get expected response code") self.assertTitleExists("Die von Blumenbach gegründete anthropologische Sammlung der Universität", location) - - self.log("Passed") \ No newline at end of file diff --git a/transaction_tests.py b/transaction_tests.py index d271c8e..f01e0a8 100644 --- a/transaction_tests.py +++ b/transaction_tests.py @@ -26,8 +26,6 @@ def get_transaction_provider(self): @Test def doCommitTest(self): - self.log("Running doCommitTest") - tx_provider = self.get_transaction_provider() if tx_provider is None: self.log("Could not location transaction provider") @@ -66,12 +64,8 @@ def doCommitTest(self): r = self.do_get(outside_location) self.assertEqual(200, r.status_code, "Did not get expected response code") - self.log("Passed") - @Test def doRollbackTest(self): - self.log("Running doRollbackTest") - tx_provider = self.get_transaction_provider() if tx_provider is None: self.log("Could not location transaction provider") @@ -105,5 +99,3 @@ def doRollbackTest(self): self.log("Container is still not available outside the transaction") r = self.do_get(outside_location) self.assertEqual(404, r.status_code, "Did not get expected response code") - - self.log("Passed") diff --git a/version_tests.py b/version_tests.py index f6e1e4e..d70a158 100644 --- a/version_tests.py +++ b/version_tests.py @@ -19,7 +19,6 @@ def count_mementos(response): @Test def doContainerVersioningTest(self): - self.log("Running doContainerVersioningTest") headers = { 'Link': self.make_type(TestConstants.LDP_BASIC) } @@ -126,12 +125,8 @@ def doContainerVersioningTest(self): r = self.do_get(memento_location) self.assertEqual(200, r.status_code, "Memento was not found") - self.log("Passed") - @Test def doBinaryVersioningTest(self): - self.log("Running doBinaryVersioningTest") - headers = { 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE), 'Content-Type': 'text/csv' @@ -226,6 +221,3 @@ def doBinaryVersioningTest(self): self.log("Check the memento exists again") r = self.do_head(memento_location) self.assertEqual(200, r.status_code, "Memento was not found") - - self.log("Passed") - From 047bdec1f9d471491d8fa69475c61a61715d59c6 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 7 Jun 2019 13:50:43 -0500 Subject: [PATCH 04/20] More cleaning and new tests --- TestConstants.py | 5 + abstract_fedora_tests.py | 15 ++- authz_tests.py | 247 +++++++++++++++++++++++++++++---------- version_tests.py | 197 +++++++++++++++++++++++++------ 4 files changed, 366 insertions(+), 98 deletions(-) diff --git a/TestConstants.py b/TestConstants.py index fa66611..0c42113 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -36,6 +36,7 @@ TURTLE_MIMETYPE = "text/turtle" SPARQL_QUERY_MIMETYPE = "application/sparql-query" SPARQL_RESULT_JSON_MIMETYPE = "application/sparql-results+json" +LINK_FORMAT_MIMETYPE = "application/link-format" LDP_NS = "http://www.w3.org/ns/ldp#" LDP_CONTAINER = LDP_NS + "Container" @@ -50,6 +51,10 @@ MEM_TIMEGATE = MEMENTO_NS + "TimeGate" MEM_TIMEMAP = MEMENTO_NS + "TimeMap" +ACL_NS = "http://www.w3.org/ns/auth/acl#" + +PURL_NS = "http://purl.org/dc/elements/1.1/" + # Test constructs OBJECT_TTL = "@prefix dc: ." \ "@prefix pcdm: ." \ diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index 83aa5c3..f1a4afb 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -147,6 +147,7 @@ def do_options(self, url, admin=True): return requests.options(url, auth=my_auth) def assert_regex_in(self, pattern, container, msg): + """ Do a regex match against all members of a list """ for i in container: if re.search(pattern, i): return @@ -155,13 +156,15 @@ def assert_regex_in(self, pattern, container, msg): self.fail(self._formatMessage(msg, standard_msg)) def assert_regex_matches(self, pattern, text, msg): + """ Do a regex match against a string """ if re.search(pattern, text): return standard_msg = '%s pattern not matched in %s' % (unittest.util.safe_repr(pattern), unittest.util.safe_repr(text)) self.fail(self._formatMessage(msg, standard_msg)) - def make_type(self, type): + @staticmethod + def make_type(type): """ Turn a URI to Link type format """ return "<{0}>; rel=\"type\"".format(type) @@ -279,7 +282,8 @@ def assertTitleExists(self, expected, location): return self.fail("Did not find expected title \"{0}\" in response".format(expected)) - def log(self, message): + @staticmethod + def log(message): print(message) def find_binary_description(self, response): @@ -287,6 +291,13 @@ def find_binary_description(self, response): self.assertIsNotNone(headers['describedby']) return headers['describedby'][0] + def checkResponse(self, expected, response): + self.checkValue(expected, response.status_code) + + def checkValue(self, expected, received): + self.assertEqual(expected, received, "Did not get expected value") + FedoraTests.log(" Passed {0} == {0}".format(received)) + def Test(func): """ Decorator for isolating test functions """ diff --git a/authz_tests.py b/authz_tests.py index 1da9ac8..76a3b9a 100644 --- a/authz_tests.py +++ b/authz_tests.py @@ -34,12 +34,17 @@ def verifyAuthEnabled(self): temp_auth = FedoraTests.create_auth(random_string, random_string) r = self.do_get(self.getFedoraBase(), admin=temp_auth) - self.assertEqual(401, r.status_code, "Did not get expected response code") + if 401 != r.status_code: + self.log("It appears that authentication is not enabled on your repository.") + quit() - def getAclUri(self, response): + @staticmethod + def getAclUri(response): acls = FedoraAuthzTests.get_link_headers(response) - self.assertIsNotNone(acls['acl']) - return acls['acl'][0] + try: + return acls['acl'][0] + except KeyError: + Exception("No acl link header found") @Test def doAuthTests(self): @@ -47,9 +52,9 @@ def doAuthTests(self): self.log("Create \"cover\" container") r = self.do_put(self.getBaseUri() + "/cover") - self.assertEqual(201, r.status_code, "Did not get expected response code") - cover_location = FedoraAuthzTests.get_location(r) - cover_acl = self.getAclUri(r) + self.checkResponse(201, r) + cover_location = self.get_location(r) + cover_acl = FedoraAuthzTests.getAclUri(r) self.log("Make \"cover\" a pcdm:Object") sparql = "PREFIX pcdm: " \ @@ -59,11 +64,11 @@ def doAuthTests(self): 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(cover_location, headers=headers, body=sparql) - self.assertEqual(204, r.status_code, "Did not get expected response code") + self.checkResponse(204, r) self.log("Verify no current ACL") r = self.do_get(cover_acl) - self.assertEqual(404, r.status_code, "Did not get expected response code") + self.checkResponse(404, r) self.log("Add ACL to \"cover\"") headers = { @@ -71,51 +76,51 @@ def doAuthTests(self): } body = self.COVER_ACL.format(cover_location, self.config[TestConstants.USER_NAME_PARAM]) r = self.do_put(cover_acl, headers=headers, body=body) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) self.log("Create \"files\" inside \"cover\"") r = self.do_put(cover_location + "/files") - self.assertEqual(201, r.status_code, "Did not get expected response code") - files_location = FedoraAuthzTests.get_location(r) - files_acl = self.getAclUri(r) + self.checkResponse(201, r) + files_location = self.get_location(r) + files_acl = FedoraAuthzTests.getAclUri(r) self.log("Anonymous can't access \"cover\"") r = self.do_get(cover_location, admin=None) - self.assertEqual(401, r.status_code, "Did not get expected response code") + self.checkResponse(401, r) self.log("Anonymous can't access \"cover/files\"") r = self.do_get(files_location, admin=None) - self.assertEqual(401, r.status_code, "Did not get expected response code") + self.checkResponse(401, r) self.log("{0} can access \"cover\"".format(self.config[TestConstants.ADMIN_USER_PARAM])) r = self.do_get(cover_location) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(200, r) self.log("{0} can access \"cover/files\"".format(self.config[TestConstants.ADMIN_USER_PARAM])) r = self.do_get(files_location) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(200, r) self.log("{0} can access \"cover\"".format(self.config[TestConstants.USER_NAME_PARAM])) r = self.do_get(cover_location, admin=False) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(200, r) self.log("{0} can access \"cover/files\"".format(self.config[TestConstants.USER_NAME_PARAM])) r = self.do_get(files_location, admin=False) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(200, r) auth = self.create_auth(self.config[TestConstants.USER2_NAME_PARAM], self.config[TestConstants.USER2_PASS_PARAM]) self.log("{0} can't access \"cover\"".format(self.config[TestConstants.USER2_NAME_PARAM])) r = self.do_get(cover_location, admin=auth) - self.assertEqual(403, r.status_code, "Did not get expected response code") + self.checkResponse(403, r) self.log("{0} can't access \"cover/files\"".format(self.config[TestConstants.USER2_NAME_PARAM])) r = self.do_get(files_location, admin=auth) - self.assertEqual(403, r.status_code, "Did not get expected response code") + self.checkResponse(403, r) self.log("Verify \"cover/files\" has no ACL") r = self.do_get(files_acl) - self.assertEqual(404, r.status_code, "Did not get expected response code") + self.checkResponse(404, r) self.log("PUT Acl to \"cover/files\" to allow access for {0}".format(self.config[TestConstants.USER2_NAME_PARAM])) headers = { @@ -123,15 +128,15 @@ def doAuthTests(self): } body = self.FILES_ACL.format(files_location, self.config[TestConstants.USER2_NAME_PARAM]) r = self.do_put(files_acl, headers=headers, body=body) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) self.log("{0} can't access \"cover\"".format(self.config[TestConstants.USER2_NAME_PARAM])) r = self.do_get(cover_location, admin=auth) - self.assertEqual(403, r.status_code, "Did not get expected response code") + self.checkResponse(403, r) self.log("{0} can access \"cover/files\"".format(self.config[TestConstants.USER2_NAME_PARAM])) r = self.do_get(files_location, admin=auth) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(200, r) @Test def doDirectIndirectAuthTests(self): @@ -139,82 +144,84 @@ def doDirectIndirectAuthTests(self): self.log("Create a target container") r = self.do_post() - self.assertEqual(201, r.status_code, "Did not get expected response code") - target_location = FedoraAuthzTests.get_location(r) - target_acl = self.getAclUri(r) + self.checkResponse(201, r) + target_location = self.get_location(r) + target_acl = FedoraAuthzTests.getAclUri(r) self.log("Create a write container") r = self.do_post() - self.assertEqual(201, r.status_code, "Did not get expected response code") - write_location = FedoraAuthzTests.get_location(r) - write_acl = self.getAclUri(r) + self.checkResponse(201, r) + write_location = self.get_location(r) + write_acl = FedoraAuthzTests.getAclUri(r) self.log("Make sure the /target resource is readonly") - target_ttl = "@prefix acl: .\n"\ + target_ttl = "@prefix acl: <{2}> .\n"\ "<#readauthz> a acl:Authorization ;\n" \ " acl:agent \"{0}\" ;\n" \ " acl:mode acl:Read ;\n" \ - " acl:accessTo <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], target_location) + " acl:accessTo <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], target_location, + TestConstants.ACL_NS) headers = { 'Content-type': 'text/turtle' } r = self.do_put(target_acl, headers=headers, body=target_ttl) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) self.log("Make sure the write resource is writable by \"{0}\"".format(self.config[TestConstants.USER_NAME_PARAM])) - write_ttl = "@prefix acl: .\n" \ + write_ttl = "@prefix acl: <{2}> .\n" \ "<#writeauth> a acl:Authorization ;\n" \ " acl:agent \"{0}\" ;\n" \ " acl:mode acl:Read, acl:Write ;\n" \ " acl:accessTo <{1}> ;\n" \ - " acl:default <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], write_location) + " acl:default <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], write_location, + TestConstants.ACL_NS) r = self.do_put(write_acl, headers=headers, body=write_ttl) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) self.log("Verify that \"{0}\" can create a simple resource under write resource (POST)".format(self.config[TestConstants.USER_NAME_PARAM])) r = self.do_post(write_location, admin=False) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) uuid_value = str(uuid.uuid4()) self.log("Verify that \"{0}\" can create a simple resource under write resource (PUT)".format( self.config[TestConstants.USER_NAME_PARAM])) r = self.do_put(write_location + "/" + uuid_value, admin=False) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) self.log("Verify that \"{0}\" CANNOT create a resource under target resource".format(self.config[TestConstants.USER_NAME_PARAM])) r = self.do_post(target_location, admin=False) - self.assertEqual(403, r.status_code, "Did not get expected response code") + self.checkResponse(403, r) self.log("Verify that \"{0}\" CANNOT create direct or indirect containers that reference target resources".format(self.config[TestConstants.USER_NAME_PARAM])) headers = { 'Content-type': 'text/turtle', 'Link': self.make_type(TestConstants.LDP_DIRECT) } - direct_ttl = "@prefix ldp: .\n" \ + direct_ttl = "@prefix ldp: <{0}> .\n" \ "@prefix test: .\n" \ - "<> ldp:membershipResource <{0}> ;\n" \ - "ldp:hasMemberRelation test:predicateToCreate .\n".format(target_location) + "<> ldp:membershipResource <{1}> ;\n" \ + "ldp:hasMemberRelation test:predicateToCreate .\n".format(TestConstants.LDP_NS, target_location) r = self.do_post(write_location, headers=headers, body=direct_ttl, admin=False) - self.assertEqual(403, r.status_code, "Did not get expected response code") + self.checkResponse(403, r) headers = { 'Content-type': 'text/turtle', 'Link': self.make_type(TestConstants.LDP_INDIRECT) } - indirect_ttl = "@prefix ldp: .\n" \ + indirect_ttl = "@prefix ldp: <{0}> .\n" \ "@prefix test: .\n" \ "<> ldp:insertedContentRelation test:something ;\n" \ - "ldp:membershipResource <{0}> ;\n" \ - "ldp:hasMemberRelation test:predicateToCreate .\n".format(target_location) + "ldp:membershipResource <{1}> ;\n" \ + "ldp:hasMemberRelation test:predicateToCreate .\n".format(TestConstants.LDP_NS, target_location) r = self.do_post(write_location, headers=headers, body=indirect_ttl, admin=False) - self.assertEqual(403, r.status_code, "Did not get expected response code") + self.checkResponse(403, r) self.log("Go ahead and create the indirect and direct containers as admin") r = self.do_post(write_location, headers=headers, body=direct_ttl) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) direct_location = self.get_location(r) r = self.do_post(write_location, headers=headers, body=indirect_ttl) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) indirect_location = self.get_location(r) self.log("Attempt to verify that \"{0}\" can not actually create relationships on the readonly resource via " \ @@ -226,13 +233,13 @@ def doDirectIndirectAuthTests(self): self.log("Verify that \"{0}\" can still create a simple resource under write resource (POST)".format(self.config[TestConstants.USER_NAME_PARAM])) r = self.do_post(write_location, admin=False) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) uuid_value = str(uuid.uuid4()) self.log("Verify that \"{0}\" can still create a simple resource under write resource (PUT)".format( self.config[TestConstants.USER_NAME_PARAM])) r = self.do_put(write_location + "/" + uuid_value, admin=False) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) @Test def multipleAuthzCreatePermissiveSet(self): @@ -240,11 +247,11 @@ def multipleAuthzCreatePermissiveSet(self): self.log("Create a target container") r = self.do_post() - self.assertEqual(201, r.status_code, "Did not get expected response code") - target_location = FedoraAuthzTests.get_location(r) - target_acl = self.getAclUri(r) + self.checkResponse(201, r) + target_location = self.get_location(r) + target_acl = FedoraAuthzTests.getAclUri(r) - double_ttl = "@prefix acl: .\n" \ + double_ttl = "@prefix acl: <{2}> .\n" \ "<#readonly> a acl:Authorization ;\n" \ " acl:agent \"{0}\" ;\n" \ " acl:mode acl:Read ;\n" \ @@ -252,23 +259,141 @@ def multipleAuthzCreatePermissiveSet(self): "<#readwrite> a acl:Authorization ;\n" \ " acl:agent \"{0}\" ;\n" \ " acl:mode acl:Write ;\n" \ - " acl:accessTo <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], target_location) + " acl:accessTo <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], target_location, + TestConstants.ACL_NS) headers = { 'Content-type': TestConstants.TURTLE_MIMETYPE } self.log("Add ACL with one read and one read/write authz.") r = self.do_put(target_acl, headers=headers, body=double_ttl) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) self.log("Check we can read") r = self.do_get(target_location, admin=False) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(200, r) self.log("Check we can write") headers = { 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE } - body = "prefix dc: INSERT { <> dc:title \"A new title\" } WHERE {}" + body = "prefix dc: <{0}> INSERT {{ <> dc:title \"A new title\" }} WHERE {{}}".format(TestConstants.PURL_NS) r = self.do_patch(target_location, headers=headers, body=body) - self.assertEqual(204, r.status_code, "Did not get expected response code") + self.checkResponse(204, r) + + @Test + def testAllThingsPointTogether(self): + self.verifyAuthEnabled() + + self.log("Create a target binary") + headers = { + 'Content-type': 'text/plain', + 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE) + } + r = self.do_post(headers=headers, body="this is a test payload") + self.checkResponse(201, r) + binary_location = self.get_location(r) + expected_acl = binary_location + "/fcr:acl" + acl_location = FedoraAuthzTests.getAclUri(r) + binary_description = self.find_binary_description(r) + + self.assertNotEqual(binary_location, binary_description) + + self.log("Check binary's acl link header is correct.") + self.checkValue(expected_acl, acl_location) + + self.log("Check binary description's acl link header is correct") + + r = self.do_get(binary_location) + self.checkResponse(200, r) + acl_location = FedoraAuthzTests.getAclUri(r) + self.checkValue(expected_acl, acl_location) + + self.log("Check binary timemap's acl link header is correct.") + binary_versions = binary_location + "/fcr:versions" + r = self.do_get(binary_versions) + self.checkResponse(200, r) + acl_location = FedoraAuthzTests.getAclUri(r) + self.checkValue(expected_acl, acl_location) + + self.log("Create a version of binary") + r = self.do_post(parent=binary_versions) + self.checkResponse(201, r) + memento_location = self.get_location(r) + + self.log("Check memento's acl link header is correct") + r = self.do_get(memento_location) + self.checkResponse(200, r) + acl_location = FedoraAuthzTests.getAclUri(r) + self.checkValue(expected_acl, acl_location) + + self.log("Check binary description timemap's acl link header is correct.") + binary_metadata_versions = binary_description + "/fcr:versions" + r = self.do_get(binary_metadata_versions) + self.checkResponse(200, r) + acl_location = FedoraAuthzTests.getAclUri(r) + self.checkValue(expected_acl, acl_location) + + # metadata version is same datetime, so create it. + metadata_memento_location = binary_metadata_versions + "/" + memento_location[memento_location.rfind('/')+1:] + + self.log("Check metadata memento's acl link header is correct") + r = self.do_get(metadata_memento_location) + self.checkResponse(200, r) + acl_location = FedoraAuthzTests.getAclUri(r) + self.checkValue(expected_acl, acl_location) + + self.log("Try to get the ACL") + r = self.do_get(acl_location) + self.checkResponse(404, r) + + @Test + def binaryAndMetadataShareACL(self): + self.verifyAuthEnabled() + + self.log("Create a target binary") + headers = { + 'Content-type': 'text/plain', + 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE) + } + r = self.do_post(headers=headers, body="this is a test payload") + self.checkResponse(201, r) + binary_location = self.get_location(r) + acl_location = FedoraAuthzTests.getAclUri(r) + binary_description = self.find_binary_description(r) + + self.log("Add ACL allowing write to metadata but not binary for user") + binary_acl = "@prefix acl: <{0}> .\n" \ + "<#binary> a acl:Authorization ;\n" \ + " acl:mode acl:Write ;\n" \ + " acl:accessTo <{1}> ;\n" \ + " acl:agent \"{2}\" .\n".format(TestConstants.ACL_NS, binary_location, + self.config[TestConstants.USER_NAME_PARAM]) + headers = { + 'Content-type': TestConstants.TURTLE_MIMETYPE + } + r = self.do_put(acl_location, headers=headers, body=binary_acl) + self.checkResponse(201, r) + + self.log("Try to read binary") + r = self.do_head(binary_location, admin=False) + self.checkResponse(403, r) + + self.log("Try to write binary") + headers = { + 'Content-type': 'text/plain' + } + r = self.do_put(binary_location, headers=headers, body="This is a new body", admin=False) + self.checkResponse(204, r) + + self.log("Try to read the metadata") + r = self.do_get(binary_description, admin=False) + self.checkResponse(403, r) + + self.log("Try to patch metadata") + patch_body = "prefix dc: <{0}> INSERT DATA {{ <> dc:title \"Updated title\"}}".format(TestConstants.PURL_NS) + headers = { + 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + } + r = self.do_patch(binary_description, patch_body, headers=headers, admin=False) + self.checkResponse(204, r) diff --git a/version_tests.py b/version_tests.py index d70a158..94e651c 100644 --- a/version_tests.py +++ b/version_tests.py @@ -17,30 +17,30 @@ def count_mementos(response): mementos = [x for x in body.split('\n') if x.find("rel=\"memento\"") >= 0] return len(mementos) - @Test + #@Test def doContainerVersioningTest(self): headers = { 'Link': self.make_type(TestConstants.LDP_BASIC) } r = self.do_post(self.getBaseUri(), headers=headers) self.log("Create a basic container") - self.assertEqual(201, r.status_code, "Did not create container") + self.checkResponse(201, r) location = self.get_location(r) version_endpoint = location + "/" + TestConstants.FCR_VERSIONS r = self.do_get(version_endpoint) - self.assertEqual(200, r.status_code, "Did not find versions where we should") + self.checkResponse(200, r) self.log("Create a version") r = self.do_post(version_endpoint) - self.assertEqual(201, r.status_code, 'Did not create a version') + self.checkResponse(201, r) self.log("Get the resource content") headers = { 'Accept': TestConstants.JSONLD_MIMETYPE } r = self.do_get(location, headers=headers) - self.assertEqual(200, r.status_code, "Could not get the resource") + self.checkResponse(200, r) body = r.content.decode('UTF-8').rstrip('\n') new_date = FedoraTests.get_rfc_date("2000-06-01 08:21:00") @@ -52,19 +52,19 @@ def doContainerVersioningTest(self): 'Memento-Datetime': new_date } r = self.do_post(version_endpoint, headers=headers, body="[]") - self.assertEqual(400, r.status_code, "Empty body should cause Bad request") + self.checkResponse(400, r) r = self.do_post(version_endpoint, headers=headers, body=body) - self.assertEqual(201, r.status_code, "Did not create memento will body.") + self.checkResponse(201, r) memento_location = self.get_location(r) self.log("Try creating another version at the same location") r = self.do_post(version_endpoint, headers=headers, body=body) - self.assertEqual(409, r.status_code, "Did not get conflict trying to create a second memento") + self.checkResponse(409, r) self.log("Check memento exists") r = self.do_get(memento_location) - self.assertEqual(200, r.status_code, "Memento was not found") + self.checkResponse(200, r) found_datetime = r.headers['Memento-Datetime'] self.assertIsNotNone(found_datetime, "Did not find Memento-Datetime header") self.assertEqual(0, self.compare_rfc_dates(new_date, found_datetime), "Returned Memento-Datetime did not match sent") @@ -78,38 +78,38 @@ def doContainerVersioningTest(self): "Content-Type": TestConstants.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=headers, body=sparql_body) - self.assertEqual(204, r.status_code, "Unable to patch") + self.checkResponse(204, r) self.log("Try to patch the memento") r = self.do_patch(memento_location, headers=headers, body=sparql_body) - self.assertEqual(405, r.status_code, "Did not get denied PATCH request.") + self.checkResponse(405, r) # Delay one second to ensure we don't POST at the same time time.sleep(1) self.log("Create another version") r = self.do_post(version_endpoint) - self.assertEqual(201, r.status_code, 'Did not create a version') + self.checkResponse(201, r) self.log("Count mementos") headers = { - 'Accept': 'application/link-format' + 'Accept': TestConstants.LINK_FORMAT_MIMETYPE } r = self.do_get(version_endpoint, headers=headers) - self.assertEqual(200, r.status_code, "Did not get the fcr:versions content") + self.checkResponse(200, r) self.assertEqual(3, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos") self.log("Delete a Memento") r = self.do_delete(memento_location) - self.assertEqual(204, r.status_code, "Did not delete the Memento") + self.checkResponse(204, r) self.log("Validate delete") r = self.do_get(memento_location) - self.assertEqual(404, r.status_code, "Memento was not gone") + self.checkResponse(404, r) self.log("Validate delete with another count") r = self.do_get(version_endpoint, headers=headers) - self.assertEqual(200, r.status_code, "Did not get the fcr:versions content") + self.checkResponse(200, r) self.assertEqual(2, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos") self.log("Create a memento at the deleted datetime") @@ -119,11 +119,11 @@ def doContainerVersioningTest(self): 'Memento-Datetime': new_date } r = self.do_post(version_endpoint, headers=headers, body=body) - self.assertEqual(201, r.status_code, "Did not create version with specific date and body") + self.checkResponse(201, r) self.log("Check the memento exists again") r = self.do_get(memento_location) - self.assertEqual(200, r.status_code, "Memento was not found") + self.checkResponse(200, r) @Test def doBinaryVersioningTest(self): @@ -136,16 +136,34 @@ def doBinaryVersioningTest(self): r = self.do_post(self.getBaseUri(), headers=headers, files=files) self.log("Create a NonRdfSource") - self.assertEqual(201, r.status_code, "Did not create NonRdfSource") + self.checkResponse(201, r) location = self.get_location(r) + description_location = self.find_binary_description(r) version_endpoint = location + "/" + TestConstants.FCR_VERSIONS + description_version_endpoint = description_location + "/" + TestConstants.FCR_VERSIONS + + self.log("Get version endpoint") r = self.do_get(version_endpoint) - self.assertEqual(200, r.status_code, "Did not find versions where we should") + self.checkResponse(200, r) + + self.log("Get description version endpoint") + r = self.do_get(description_version_endpoint) + self.checkResponse(200, r) self.log("Create a version") r = self.do_post(version_endpoint) - self.assertEqual(201, r.status_code, 'Did not create a version') + self.checkResponse(201, r) + + self.log("Try to create another version too quickly") + r = self.do_post(version_endpoint) + self.checkResponse(409, r) + + self.assertNotEqual(version_endpoint, description_version_endpoint) + + self.log("Try to create a version of the description quickly") + r = self.do_post(description_version_endpoint) + self.checkResponse(409, r) new_date = FedoraTests.get_rfc_date("2000-06-01 08:21:00") @@ -155,16 +173,16 @@ def doBinaryVersioningTest(self): 'Memento-Datetime': new_date } r = self.do_post(version_endpoint, headers=headers, files=files) - self.assertEqual(201, r.status_code, "Did not create version with specific date and body") + self.checkResponse(201, r) memento_location = self.get_location(r) self.log("Try creating another version at the same location") r = self.do_post(version_endpoint, headers=headers, files=files) - self.assertEqual(409, r.status_code, "Did not get conflict trying to create a second memento") + self.checkResponse(409, r) self.log("Check memento exists") r = self.do_head(memento_location) - self.assertEqual(200, r.status_code, "Memento was not found") + self.checkResponse(200, r) found_datetime = r.headers['Memento-Datetime'] self.assertIsNotNone(found_datetime, "Did not find Memento-Datetime header") self.assertEqual(0, self.compare_rfc_dates(new_date, found_datetime), "Returned Memento-Datetime did not match sent") @@ -175,38 +193,38 @@ def doBinaryVersioningTest(self): 'Content-Type': 'text/csv' } r = self.do_put(location, headers=headers, files=files) - self.assertEqual(204, r.status_code, "Unable to put") + self.checkResponse(204, r) self.log("Try to put to the memento") r = self.do_put(memento_location, headers=headers, files=files) - self.assertEqual(405, r.status_code, "Did not get denied PUT request.") + self.checkResponse(405, r) # Delay one second to ensure we don't POST at the same time time.sleep(1) self.log("Create another version") r = self.do_post(version_endpoint) - self.assertEqual(201, r.status_code, 'Did not create a version') + self.checkResponse(201, r) self.log("Count mementos") headers = { - 'Accept': 'application/link-format' + 'Accept': TestConstants.LINK_FORMAT_MIMETYPE } r = self.do_get(version_endpoint, headers=headers) - self.assertEqual(200, r.status_code, "Did not get the fcr:versions content") + self.checkResponse(200, r) self.assertEqual(3, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos") self.log("Delete a Memento") r = self.do_delete(memento_location) - self.assertEqual(204, r.status_code, "Did not delete the Memento") + self.checkResponse(204, r) self.log("Validate delete") r = self.do_get(memento_location) - self.assertEqual(404, r.status_code, "Memento was not gone") + self.checkResponse(404, r) self.log("Validate delete with another count") r = self.do_get(version_endpoint, headers=headers) - self.assertEqual(200, r.status_code, "Did not get the fcr:versions content") + self.checkResponse(200, r) self.assertEqual(2, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos") self.log("Create a memento at the deleted datetime") @@ -216,8 +234,117 @@ def doBinaryVersioningTest(self): 'Memento-Datetime': new_date } r = self.do_post(version_endpoint, headers=headers, files=files) - self.assertEqual(201, r.status_code, "Did not create version with specific date and body") + self.checkResponse(201, r) self.log("Check the memento exists again") r = self.do_head(memento_location) - self.assertEqual(200, r.status_code, "Memento was not found") + self.checkResponse(200, r) + + @Test + def checkBinaryVersioning(self): + headers = { + 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE), + 'Content-Type': 'text/csv' + } + files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\n')} + + self.log("Create a NonRdfSource") + r = self.do_post(self.getBaseUri(), headers=headers, files=files) + self.checkResponse(201, r) + location = self.get_location(r) + description_location = self.find_binary_description(r) + + binary_versions = location + "/" + TestConstants.FCR_VERSIONS + metadata_versions = description_location + "/" + TestConstants.FCR_VERSIONS + + link_headers = { + 'Accept': TestConstants.LINK_FORMAT_MIMETYPE + } + + self.log("Count Mementos of binary") + r = self.do_get(binary_versions, headers=link_headers) + self.checkResponse(200, r) + self.checkValue(0, self.count_mementos(r)) + + self.log("Count Mementos of binary description") + r = self.do_get(metadata_versions, headers=link_headers) + self.checkResponse(200, r) + self.checkValue(0, self.count_mementos(r)) + + self.log("Create version of binary from existing") + r = self.do_post(binary_versions) + self.checkResponse(201, r) + + self.log("Count Mementos of binary") + r = self.do_get(binary_versions, headers=link_headers) + self.checkResponse(200, r) + self.checkValue(1, self.count_mementos(r)) + + self.log("Count Mementos of binary description") + r = self.do_get(metadata_versions, headers=link_headers) + self.checkResponse(200, r) + self.checkValue(1, self.count_mementos(r)) + + self.log("Wait a second") + time.sleep(1) + + self.log("Create version of binary metadata from existing") + r = self.do_post(metadata_versions) + self.checkResponse(201, r) + + self.log("Count Mementos of binary") + r = self.do_get(binary_versions, headers=link_headers) + self.checkResponse(200, r) + self.checkValue(1, self.count_mementos(r)) + + self.log("Count Mementos of binary description") + r = self.do_get(metadata_versions, headers=link_headers) + self.checkResponse(200, r) + self.checkValue(2, self.count_mementos(r)) + + + # Re-enable once FCREPO-3011 lands + #@Test + def createBinaryVersionsAtSameTime(self): + headers = { + 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE), + 'Content-Type': 'text/csv' + } + files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\n')} + + self.log("Create a NonRdfSource") + r = self.do_post(self.getBaseUri(), headers=headers, files=files) + self.checkResponse(201, r) + location = self.get_location(r) + description_location = self.find_binary_description(r) + + headers = { + 'Prefer': FedoraTests.make_prefer_header(TestConstants.SERVER_MANAGED, True) + } + r = self.do_get(description_location, headers=headers) + new_body = "@prefix dc: <{0}> .\n".format(TestConstants.PURL_NS) + \ + r.text[0:-2] + ";\n dc:title \"New title\" .\n".format(TestConstants.PURL_NS) + + version_endpoint = location + "/" + TestConstants.FCR_VERSIONS + description_version_endpoint = description_location + "/" + TestConstants.FCR_VERSIONS + + the_date = FedoraTests.get_rfc_date('2019-05-21 18:30:00') + files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\nevent,more,data,tosend\n')} + headers = { + 'Content-Type': 'text/csv', + 'Memento-Datetime': the_date + } + + self.log("Make version for {0} of binary".format(the_date)) + r = self.do_post(version_endpoint, headers=headers, files=files) + self.checkResponse(201, r) + + headers = { + 'Content-type': TestConstants.TURTLE_MIMETYPE, + 'Memento-Datetime': the_date, + 'Prefer': TestConstants.PUT_PREFER_LENIENT + } + + self.log("Make version for {0} of binary description".format(the_date)) + r = self.do_post(description_version_endpoint, headers=headers, body=new_body) + self.checkResponse(201, r) From 01e978b2462cd2d7d1c8aa5071c883c6b6c860ab Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 7 Jun 2019 13:54:51 -0500 Subject: [PATCH 05/20] Forgot to re-enable a test --- version_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version_tests.py b/version_tests.py index 94e651c..0f9af10 100644 --- a/version_tests.py +++ b/version_tests.py @@ -17,7 +17,7 @@ def count_mementos(response): mementos = [x for x in body.split('\n') if x.find("rel=\"memento\"") >= 0] return len(mementos) - #@Test + @Test def doContainerVersioningTest(self): headers = { 'Link': self.make_type(TestConstants.LDP_BASIC) From a79aa9d4fa099d5475ab3727ca74ed5b18705ca2 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 7 Jun 2019 15:40:01 -0500 Subject: [PATCH 06/20] Refactor and must get server triples for a Memento POST --- version_tests.py | 75 ++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/version_tests.py b/version_tests.py index 0f9af10..f171fbf 100644 --- a/version_tests.py +++ b/version_tests.py @@ -17,6 +17,18 @@ def count_mementos(response): mementos = [x for x in body.split('\n') if x.find("rel=\"memento\"") >= 0] return len(mementos) + def checkMementoCount(self, expected, uri, admin=None): + if admin is None: + admin = True + if not uri.endswith("/" + TestConstants.FCR_VERSIONS): + uri += "/" + TestConstants.FCR_VERSIONS + headers = { + 'Accept': TestConstants.LINK_FORMAT_MIMETYPE + } + r = self.do_get(uri, headers=headers, admin=admin) + self.checkResponse(200, r) + self.checkValue(expected, self.count_mementos(r)) + @Test def doContainerVersioningTest(self): headers = { @@ -45,15 +57,16 @@ def doContainerVersioningTest(self): new_date = FedoraTests.get_rfc_date("2000-06-01 08:21:00") - self.log("Create a version with provided datetime") headers = { 'Content-Type': TestConstants.JSONLD_MIMETYPE, 'Prefer': TestConstants.PUT_PREFER_LENIENT, 'Memento-Datetime': new_date } + self.log("Check you must provide a valid body") r = self.do_post(version_endpoint, headers=headers, body="[]") self.checkResponse(400, r) + self.log("Create a version with provided datetime") r = self.do_post(version_endpoint, headers=headers, body=body) self.checkResponse(201, r) memento_location = self.get_location(r) @@ -84,7 +97,7 @@ def doContainerVersioningTest(self): r = self.do_patch(memento_location, headers=headers, body=sparql_body) self.checkResponse(405, r) - # Delay one second to ensure we don't POST at the same time + self.log("Wait a second to change the time.") time.sleep(1) self.log("Create another version") @@ -92,12 +105,7 @@ def doContainerVersioningTest(self): self.checkResponse(201, r) self.log("Count mementos") - headers = { - 'Accept': TestConstants.LINK_FORMAT_MIMETYPE - } - r = self.do_get(version_endpoint, headers=headers) - self.checkResponse(200, r) - self.assertEqual(3, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos") + self.checkMementoCount(3, version_endpoint) self.log("Delete a Memento") r = self.do_delete(memento_location) @@ -108,9 +116,7 @@ def doContainerVersioningTest(self): self.checkResponse(404, r) self.log("Validate delete with another count") - r = self.do_get(version_endpoint, headers=headers) - self.checkResponse(200, r) - self.assertEqual(2, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos") + self.checkMementoCount(2, version_endpoint) self.log("Create a memento at the deleted datetime") headers = { @@ -199,7 +205,7 @@ def doBinaryVersioningTest(self): r = self.do_put(memento_location, headers=headers, files=files) self.checkResponse(405, r) - # Delay one second to ensure we don't POST at the same time + self.log("Wait one second to change the time.") time.sleep(1) self.log("Create another version") @@ -207,12 +213,7 @@ def doBinaryVersioningTest(self): self.checkResponse(201, r) self.log("Count mementos") - headers = { - 'Accept': TestConstants.LINK_FORMAT_MIMETYPE - } - r = self.do_get(version_endpoint, headers=headers) - self.checkResponse(200, r) - self.assertEqual(3, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos") + self.checkMementoCount(3, version_endpoint) self.log("Delete a Memento") r = self.do_delete(memento_location) @@ -223,9 +224,7 @@ def doBinaryVersioningTest(self): self.checkResponse(404, r) self.log("Validate delete with another count") - r = self.do_get(version_endpoint, headers=headers) - self.checkResponse(200, r) - self.assertEqual(2, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos") + self.checkMementoCount(2, version_endpoint) self.log("Create a memento at the deleted datetime") headers = { @@ -240,6 +239,9 @@ def doBinaryVersioningTest(self): r = self.do_head(memento_location) self.checkResponse(200, r) + self.log("Validate count of mementos again") + self.checkMementoCount(3, version_endpoint) + @Test def checkBinaryVersioning(self): headers = { @@ -276,14 +278,10 @@ def checkBinaryVersioning(self): self.checkResponse(201, r) self.log("Count Mementos of binary") - r = self.do_get(binary_versions, headers=link_headers) - self.checkResponse(200, r) - self.checkValue(1, self.count_mementos(r)) + self.checkMementoCount(1, binary_versions) self.log("Count Mementos of binary description") - r = self.do_get(metadata_versions, headers=link_headers) - self.checkResponse(200, r) - self.checkValue(1, self.count_mementos(r)) + self.checkMementoCount(1, metadata_versions) self.log("Wait a second") time.sleep(1) @@ -293,18 +291,12 @@ def checkBinaryVersioning(self): self.checkResponse(201, r) self.log("Count Mementos of binary") - r = self.do_get(binary_versions, headers=link_headers) - self.checkResponse(200, r) - self.checkValue(1, self.count_mementos(r)) + self.checkMementoCount(1, binary_versions) self.log("Count Mementos of binary description") - r = self.do_get(metadata_versions, headers=link_headers) - self.checkResponse(200, r) - self.checkValue(2, self.count_mementos(r)) + self.checkMementoCount(2, metadata_versions) - - # Re-enable once FCREPO-3011 lands - #@Test + @Test def createBinaryVersionsAtSameTime(self): headers = { 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE), @@ -318,10 +310,7 @@ def createBinaryVersionsAtSameTime(self): location = self.get_location(r) description_location = self.find_binary_description(r) - headers = { - 'Prefer': FedoraTests.make_prefer_header(TestConstants.SERVER_MANAGED, True) - } - r = self.do_get(description_location, headers=headers) + r = self.do_get(description_location) new_body = "@prefix dc: <{0}> .\n".format(TestConstants.PURL_NS) + \ r.text[0:-2] + ";\n dc:title \"New title\" .\n".format(TestConstants.PURL_NS) @@ -348,3 +337,9 @@ def createBinaryVersionsAtSameTime(self): self.log("Make version for {0} of binary description".format(the_date)) r = self.do_post(description_version_endpoint, headers=headers, body=new_body) self.checkResponse(201, r) + + self.log("Count binary mementos") + self.checkMementoCount(1, version_endpoint) + + self.log("Count binary description mementos") + self.checkMementoCount(1, description_version_endpoint) From f875396b0409972423cceea909d3c6778206ecdd Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 7 Jun 2019 15:50:06 -0500 Subject: [PATCH 07/20] Some more version test checks --- authz_tests.py | 4 ++-- version_tests.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/authz_tests.py b/authz_tests.py index 76a3b9a..af22b33 100644 --- a/authz_tests.py +++ b/authz_tests.py @@ -265,7 +265,7 @@ def multipleAuthzCreatePermissiveSet(self): 'Content-type': TestConstants.TURTLE_MIMETYPE } - self.log("Add ACL with one read and one read/write authz.") + self.log("Add ACL with one read and one write authz for the same URI.") r = self.do_put(target_acl, headers=headers, body=double_ttl) self.checkResponse(201, r) @@ -335,7 +335,7 @@ def testAllThingsPointTogether(self): self.checkValue(expected_acl, acl_location) # metadata version is same datetime, so create it. - metadata_memento_location = binary_metadata_versions + "/" + memento_location[memento_location.rfind('/')+1:] + metadata_memento_location = binary_metadata_versions + memento_location[memento_location.rfind('/'):] self.log("Check metadata memento's acl link header is correct") r = self.do_get(metadata_memento_location) diff --git a/version_tests.py b/version_tests.py index f171fbf..25f78d9 100644 --- a/version_tests.py +++ b/version_tests.py @@ -317,6 +317,10 @@ def createBinaryVersionsAtSameTime(self): version_endpoint = location + "/" + TestConstants.FCR_VERSIONS description_version_endpoint = description_location + "/" + TestConstants.FCR_VERSIONS + self.log("Check we have no mementos of binary or description") + self.checkMementoCount(0, version_endpoint) + self.checkMementoCount(0, description_version_endpoint) + the_date = FedoraTests.get_rfc_date('2019-05-21 18:30:00') files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\nevent,more,data,tosend\n')} headers = { @@ -328,6 +332,11 @@ def createBinaryVersionsAtSameTime(self): r = self.do_post(version_endpoint, headers=headers, files=files) self.checkResponse(201, r) + self.log("Check we only made a memento of the binary") + self.checkMementoCount(1, version_endpoint) + self.log("Check we still have no description mementos") + self.checkMementoCount(0, description_version_endpoint) + headers = { 'Content-type': TestConstants.TURTLE_MIMETYPE, 'Memento-Datetime': the_date, From d770c7c0ebf6b35a61118a50c9e3cdef13f2b209 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Sat, 8 Jun 2019 15:53:55 -0500 Subject: [PATCH 08/20] Add authz accessToClass test Update README somewhat --- README.md | 40 ++++++++++++++++++++++ TestConstants.py | 8 +++-- abstract_fedora_tests.py | 5 +++ authz_tests.py | 73 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5290871..1aec699 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,34 @@ Both of these systems must be fed by the fcrepo-camel-toolbox for this testing. 1. Verify regular user 1 can access **cover** 1. Verify regular user 2 can't access **cover** + +1. Create a container that is readonly for regular user 1 +1. Create a container that regular user 1 has read/write access to. +1. Verify that regular user 1 can create/edit/append the container +1. Verify that regular user 1 cannot create a direct or indirect container +that targets the read-only container as the membership resource. + + +1. Create a container +1. Add an acl with multiple authorizations for user 1 +1. Verify that user 1 receives the most permissive set of permissions +from the authorizations + + +1. Verify that the `rel="acl"` link header is the same for: + * a binary + * its description + * the binary timemap + * the description timemap + * a binary memento + * a description memento + + +1. Verify that both a binary and its description share the permissions give +to the binary. + + + ### basic 1. Create a container 1. Create a container inside the container from step 1 @@ -133,6 +161,18 @@ Both of these systems must be fed by the fcrepo-camel-toolbox for this testing. 1. Create a LDP Indirect container 1. Validate the correct Link header type + +1. Create a basic container +1. Create an indirect container +1. Create a direct container +1. Create a NonRDFSource +1. Try to create a ldp:Resource (not allowed) +1. Try to create a ldp:Container (not allowed) + + +1. Try to change each of the following (basic, direct, indirect container +and binary) to all other types. + ### camel - see [note](#camel-tests) 1. Create a container 1. Check the container is indexed to Solr diff --git a/TestConstants.py b/TestConstants.py index 0c42113..382456b 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -16,11 +16,12 @@ PAYLOAD_HEADERS = ['Content-Length', 'Content-Range', 'Trailer', 'Transfer-Encoding'] # Fedora specific constants +FEDORA_NS = "http://fedora.info/definitions/v4/repository#" FCR_VERSIONS = "fcr:versions" FCR_FIXITY = "fcr:fixity" -SERVER_MANAGED = "http://fedora.info/definitions/v4/repository#ServerManaged" -INBOUND_REFERENCE = "http://fedora.info/definitions/v4/repository#InboundReferences" -EMBEDED_RESOURCE = "http://fedora.info/definitions/v4/repository#EmbedResources" +SERVER_MANAGED = FEDORA_NS + "ServerManaged" +INBOUND_REFERENCE = FEDORA_NS + "InboundReferences" +EMBEDED_RESOURCE = FEDORA_NS + "EmbedResources" GET_PREFER_MINIMAL = "return=minimal" PUT_PREFER_LENIENT = "handling=lenient; received=\"minimal\"" @@ -50,6 +51,7 @@ MEM_ORIGINAL_RESOURCE = MEMENTO_NS + "OriginalResource" MEM_TIMEGATE = MEMENTO_NS + "TimeGate" MEM_TIMEMAP = MEMENTO_NS + "TimeMap" +MEM_MEMENTO = MEMENTO_NS + "Memento" ACL_NS = "http://www.w3.org/ns/auth/acl#" diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index f1a4afb..2d1f697 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -80,6 +80,11 @@ def create_user_auth(self): return FedoraTests.create_auth( self.config[TestConstants.USER_NAME_PARAM], self.config[TestConstants.USER_PASS_PARAM]) + def create_user2_auth(self): + """ Create a Basic Auth object using the test user 2 username:password """ + return FedoraTests.create_auth( + self.config[TestConstants.USER2_NAME_PARAM], self.config[TestConstants.USER2_PASS_PARAM]) + def get_auth(self, admin=True): """ The admin argument of this function is used through out the testing infrastructure, it is explained here. True - use admin username / password credentials diff --git a/authz_tests.py b/authz_tests.py index af22b33..d0ada43 100644 --- a/authz_tests.py +++ b/authz_tests.py @@ -397,3 +397,76 @@ def binaryAndMetadataShareACL(self): } r = self.do_patch(binary_description, patch_body, headers=headers, admin=False) self.checkResponse(204, r) + + @Test + def testContainerWithAccessToClass(self): + + self.verifyAuthEnabled() + + self.log("Create a container") + r = self.do_post() + location = self.get_location(r) + acl_location = self.getAclUri(r) + versions_location = location + "/" + TestConstants.FCR_VERSIONS + + full_acl = "@prefix acl: <{0}> .\n" \ + "@prefix fedora: <{1}> .\n" \ + "@prefix memento: <{2}> .\n" \ + "<#container> a acl:Authorization ;\n" \ + " acl:mode acl:Read ;\n" \ + " acl:accessTo <{3}> ;\n" \ + " acl:agent \"{4}\" .\n" \ + "<#timemap> a acl:Authorization ;\n" \ + " acl:mode acl:Read ;\n" \ + " acl:accessToClass fedora:TimeMap ;\n" \ + " acl:default <{3}> ;\n" \ + " acl:agent \"{4}\" .\n" \ + "<#memento> a acl:Authorization ;\n" \ + " acl:mode acl:Read ;\n" \ + " acl:accessToClass memento:Memento ;\n" \ + " acl:default <{3}> ;\n" \ + " acl:agent \"{5}\" .\n".format(TestConstants.ACL_NS, TestConstants.FEDORA_NS, TestConstants.MEMENTO_NS, + location, self.config[TestConstants.USER_NAME_PARAM], + self.config[TestConstants.ADMIN_USER_PARAM]) + turtle_headers = { + 'Content-type': TestConstants.TURTLE_MIMETYPE + } + + user2 = self.create_user2_auth() + + self.log("Put ACL giving test user 1 read access to container and timemap but not mementos") + r = self.do_put(acl_location, headers=turtle_headers, body=full_acl) + self.checkResponse(201, r) + + self.log("Create a Memento as admin") + r = self.do_post(versions_location) + self.checkResponse(201, r) + memento_location = self.get_location(r) + + self.log("Try to get container as test user 1") + r = self.do_get(location, admin=False) + self.checkResponse(200, r) + + self.log("Try to get container as test user 2") + r = self.do_get(location, admin=user2) + self.checkResponse(403, r) + + self.log("Try to get timemap as test user 1") + r = self.do_get(versions_location, admin=False) + self.checkResponse(200, r) + + self.log("Try to get timemap as test user 2") + r = self.do_get(versions_location, admin=user2) + self.checkResponse(403, r) + + self.log("Check that memento exists as admin") + r = self.do_get(memento_location) + self.checkResponse(200, r) + + self.log("Try to get memento as test user 1") + r = self.do_get(memento_location, admin=False) + self.checkResponse(403, r) + + self.log("Try to get memento as test user 2") + r = self.do_get(memento_location, admin=user2) + self.checkResponse(403, r) From 57bdadad511f23e5a56f87280b545d44c2cf8867 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Tue, 14 Apr 2020 11:00:52 -0500 Subject: [PATCH 09/20] Update transaction test for Fedora 6 --- TestConstants.py | 4 ++ abstract_fedora_tests.py | 23 +++++++++- requirements.txt | 6 +-- testrunner.py | 3 +- transaction_tests.py | 99 ++++++++++++++++++++++++++++------------ 5 files changed, 102 insertions(+), 33 deletions(-) diff --git a/TestConstants.py b/TestConstants.py index 382456b..c5359fb 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -19,10 +19,14 @@ FEDORA_NS = "http://fedora.info/definitions/v4/repository#" FCR_VERSIONS = "fcr:versions" FCR_FIXITY = "fcr:fixity" +FCR_TX = "fcr:tx" SERVER_MANAGED = FEDORA_NS + "ServerManaged" INBOUND_REFERENCE = FEDORA_NS + "InboundReferences" EMBEDED_RESOURCE = FEDORA_NS + "EmbedResources" +FEDORA_TX_NS = "http://fedora.info/definitions/v4/transaction#" +FEDORA_TX_ENDPOINT_REL = FEDORA_TX_NS + "endpoint" + GET_PREFER_MINIMAL = "return=minimal" PUT_PREFER_LENIENT = "handling=lenient; received=\"minimal\"" diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index 2d1f697..c8446d9 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -229,7 +229,7 @@ def get_link_headers(response): headers = {} link_headers = [x.strip() for x in response.headers['Link'].split(",")] for x in link_headers: - matches = re.match(r'\s*<([^>]+)>;\s?rel=[\'"]?(\w+)[\'"]?', x) + matches = re.match(r'\s*<([^>]+)>;\s?rel=[\'"]?([^\'"]+)[\'"]?', x) if matches is not None: try: headers[matches.group(2)] @@ -287,6 +287,27 @@ def assertTitleExists(self, expected, location): return self.fail("Did not find expected title \"{0}\" in response".format(expected)) + def assertLinkHeaderExists(self, response, rel, expected=None): + """ Check for the existence of a link header and possibly match the URI """ + link_headers = self.get_link_headers(response) + if rel not in link_headers: + self.fail("Did not find expected link header with rel={0}".format(rel)) + else: + if expected is not None: + if link_headers.get(rel) != expected: + self.fail( + "Did not find expected link header value for rel={0}, found {1} expected {2}" + .format(rel, link_headers.get(rel), expected)) + + def assertHeaderExists(self, response, name, value=None): + """ Check for the existence of a header and possibly match the value """ + if name.lower() not in response.headers: + self.fail("Did not find expected header {0}".format(name)) + else: + if value is not None and response.headers[name.lower()] != value: + self.fail("Did not find expected header value for header {0}, found {1} expected {2}" + .format(name, response.headers[name], value)) + @staticmethod def log(message): print(message) diff --git a/requirements.txt b/requirements.txt index 3a7e903..fcf1077 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -PyYAML==5.4 -requests==2.20 -pyjq==2.3.0 +PyYAML>=5.4 +requests>=2.20.0 +pyjq>=2.3.0 diff --git a/testrunner.py b/testrunner.py index aa19dc6..d0b8e98 100755 --- a/testrunner.py +++ b/testrunner.py @@ -9,6 +9,7 @@ from yaml import CLoader as Loader except ImportError: from yaml import Loader +from yaml import SafeLoader from basic_interaction_tests import FedoraBasicIxnTests from version_tests import FedoraVersionTests from fixity_tests import FedoraFixityTests @@ -47,7 +48,7 @@ def load_config(self, file, site): if os.path.exists(file): if os.access(file, os.R_OK): with open(file, 'r') as fp: - yml = load(fp.read()) + yml = load(fp.read(), Loader=SafeLoader) self.config = yml.get(site) def parse_cmdline_args(self, args): diff --git a/transaction_tests.py b/transaction_tests.py index f01e0a8..80ef229 100644 --- a/transaction_tests.py +++ b/transaction_tests.py @@ -2,8 +2,6 @@ import TestConstants from abstract_fedora_tests import FedoraTests, register_tests, Test -import json -import pyjq @register_tests @@ -16,13 +14,12 @@ def get_transaction_provider(self): headers = { 'Accept': TestConstants.JSONLD_MIMETYPE } - r = self.do_get(self.getFedoraBase(), headers=headers) + r = self.do_head(self.getFedoraBase(), headers=headers) self.assertEqual(200, r.status_code, "Did not get expected response") - body = r.content.decode('UTF-8') - json_body = json.loads(body) - tx_provider = pyjq.first('.[]."http://fedora.info/definitions/v4/repository#hasTransactionProvider" | .[]."@id"', - json_body) - return tx_provider + link_headers = self.get_link_headers(r) + if TestConstants.FEDORA_TX_ENDPOINT_REL in link_headers.keys(): + return link_headers.get(TestConstants.FEDORA_TX_ENDPOINT_REL)[0] + return None @Test def doCommitTest(self): @@ -34,36 +31,73 @@ def doCommitTest(self): self.log("Create a transaction") r = self.do_post(tx_provider) self.assertEqual(201, r.status_code, "Did not get expected response code") - full_transaction_uri = self.get_location(r) - transaction = full_transaction_uri.replace(self.getFedoraBase(), "") - self.log("Transaction is {0}".format(transaction)) + transaction_id = self.get_location(r) + self.log("Transaction is {0}".format(transaction_id)) self.log("Get status of transaction") - r = self.do_get(full_transaction_uri) - self.assertEqual(200, r.status_code, "Did not get expected response code") + r = self.do_get(transaction_id) + self.assertEqual(204, r.status_code, "Did not get expected response code") + self.assertHeaderExists(r, "Atomic-Expires") self.log("Create an container in the transaction") - r = self.do_post(full_transaction_uri + self.CONTAINER) + transaction_headers = { + 'Atomic-Id': transaction_id + } + r = self.do_post(headers=transaction_headers) self.assertEqual(201, r.status_code, "Did not get expected response code") transaction_obj = self.get_location(r) self.log("Container is available inside the transaction") - r = self.do_get(transaction_obj) + r = self.do_get(transaction_obj, headers=transaction_headers) self.assertEqual(200, r.status_code, "Did not get expected response code") self.log("Container not available outside the transaction") - outside_location = transaction_obj.replace(transaction, "") - r = self.do_get(outside_location) + r = self.do_get(transaction_obj) + self.assertEqual(404, r.status_code, "Did not get expected response code") + + self.log("Use an invalid transaction ID") + bad_headers = { + 'Atomic-ID': 'this-is-a-failure' + } + r = self.do_post(headers=bad_headers) + self.assertEqual(409, r.status_code, "Did not get expected response code") + + self.log("Use the bare UUID of a valid transaction ID") + diff_headers = { + 'Atomic-ID': transaction_id.replace(self.getFedoraBase() + "/" + TestConstants.FCR_TX + "/", "") + } + r = self.do_post(headers=diff_headers) + self.assertEqual(201, r.status_code, "Did not get expected response code") + second_obj = self.get_location(r) + + self.log("Second container is available inside the transaction") + r = self.do_get(second_obj, headers=transaction_headers) + self.assertEqual(200, r.status_code, "Did not get expected response code") + + self.log("Second container not available outside the transaction") + r = self.do_get(second_obj) self.assertEqual(404, r.status_code, "Did not get expected response code") + self.log("Try to commit with POST") + r = self.do_post(transaction_id + "/commit") + self.assertEqual(405, r.status_code, "Did not get expected response code") + self.log("Commit transaction") - r = self.do_post(full_transaction_uri + "/fcr:tx/fcr:commit") + r = self.do_put(transaction_id + "/commit") self.assertEqual(204, r.status_code, "Did not get expected response code") self.log("Container is now available outside the transaction") - r = self.do_get(outside_location) + r = self.do_get(transaction_obj) self.assertEqual(200, r.status_code, "Did not get expected response code") + self.log("Transaction is no longer available") + r = self.do_get(transaction_id) + self.assertEqual(410, r.status_code, "Did not get expected response code") + + self.log("Can't use the transaction anymore") + r = self.do_post(headers=transaction_headers) + self.assertEqual(409, r.status_code, "Did not get expected response code") + @Test def doRollbackTest(self): tx_provider = self.get_transaction_provider() @@ -74,28 +108,37 @@ def doRollbackTest(self): self.log("Create a transaction") r = self.do_post(tx_provider) self.assertEqual(201, r.status_code, "Did not get expected response code") - full_transaction_uri = self.get_location(r) - transaction = full_transaction_uri.replace(self.getFedoraBase(), "") - self.log("Transaction is {0}".format(transaction)) + transaction_id = self.get_location(r) + self.log("Transaction is {0}".format(transaction_id)) self.log("Create an container in the transaction") - r = self.do_post(full_transaction_uri + self.CONTAINER) + transaction_headers = { + 'Atomic-Id': transaction_id + } + r = self.do_post(headers=transaction_headers) self.assertEqual(201, r.status_code, "Did not get expected response code") transaction_obj = self.get_location(r) self.log("Container is available inside the transaction") - r = self.do_get(transaction_obj) + r = self.do_get(transaction_obj, headers=transaction_headers) self.assertEqual(200, r.status_code, "Did not get expected response code") self.log("Container not available outside the transaction") - outside_location = transaction_obj.replace(transaction, "") - r = self.do_get(outside_location) + r = self.do_get(transaction_obj) self.assertEqual(404, r.status_code, "Did not get expected response code") self.log("Rollback transaction") - r = self.do_post(full_transaction_uri + "/fcr:tx/fcr:rollback") + r = self.do_delete(transaction_id) self.assertEqual(204, r.status_code, "Did not get expected response code") self.log("Container is still not available outside the transaction") - r = self.do_get(outside_location) + r = self.do_get(transaction_obj) self.assertEqual(404, r.status_code, "Did not get expected response code") + + self.log("Transaction is no longer available") + r = self.do_get(transaction_id) + self.assertEqual(410, r.status_code, "Did not get expected response code") + + self.log("Can't use the transaction anymore") + r = self.do_post(headers=transaction_headers) + self.assertEqual(409, r.status_code, "Did not get expected response code") From 2b8a5be30c2e0d7bb64700c97c6201684e50e26a Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Wed, 15 Apr 2020 09:53:14 -0500 Subject: [PATCH 10/20] Add containment test --- basic_interaction_tests.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/basic_interaction_tests.py b/basic_interaction_tests.py index f0c89c3..ea05737 100644 --- a/basic_interaction_tests.py +++ b/basic_interaction_tests.py @@ -3,6 +3,8 @@ import TestConstants from abstract_fedora_tests import FedoraTests, register_tests, Test import os +import pyjq +import json @register_tests @@ -79,7 +81,7 @@ def doNestedTests(self): self.log("Create a container in a container") r = self.do_post(location, headers=headers, body=TestConstants.OBJECT_TTL) self.assertEqual(201, r.status_code, "Did not get expected status code") - location2 = self.get_location(r) + main_child1 = self.get_location(r) self.log("Create binary inside a container inside a container") with open(os.path.join(os.getcwd(), 'resources', 'basic_image.jpg'), 'rb') as fp: @@ -87,10 +89,39 @@ def doNestedTests(self): 'Content-type': 'image/jpeg' } data = fp.read() - r = self.do_post(location2, headers=headers, body=data) + r = self.do_post(main_child1, headers=headers, body=data) self.assertEqual(201, r.status_code, "Did not get expected status code") binary_location = self.get_location(r) + self.log("Create a second child in the top container") + r = self.do_post(location, headers=headers, body=TestConstants.OBJECT_TTL) + self.assertEqual(201, r.status_code, "Did not get expected status code") + main_child2 = self.get_location(r) + + self.log("Verify containment") + headers = { + 'Accept': TestConstants.JSONLD_MIMETYPE + } + r = self.do_get(location, headers=headers) + self.assertEqual(200, r.status_code, "Can't get the container") + body = r.content.decode('UTF-8').rstrip('\ny') + json_body = json.loads(body) + contained = pyjq.all('.[0]."http://www.w3.org/ns/ldp#contains"', json_body) + expected = [ + main_child1, + main_child2 + ] + found = list() + for c in contained[0]: + child_id = pyjq.first('."@id"', c) + found.append(child_id) + if len(found) != len(expected): + self.fail("Expected {0} contained resources, found {1}".format(len(expected), len(found))) + else: + for child_id in found: + if child_id not in expected: + self.fail("Found unexpected containment relationship {0}".format(child_id)) + self.log("Delete binary") r = self.do_delete(binary_location) self.assertEqual(204, r.status_code, "Did not get expected status code") @@ -104,7 +135,7 @@ def doNestedTests(self): self.assertEqual(204, r.status_code, "Did not get expected status code") self.log("Verify both are gone") - r = self.do_get(location2) + r = self.do_get(main_child1) self.assertEqual(410, r.status_code, "Did not get expected status code") r = self.do_get(location) self.assertEqual(410, r.status_code, "Did not get expected status code") From 88cff75640f30cf7e5b971647dbd7633b0beeb4b Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Thu, 30 Apr 2020 14:06:31 -0500 Subject: [PATCH 11/20] Add some more tests --- TestConstants.py | 1 + abstract_fedora_tests.py | 21 ++++++++++++++ basic_interaction_tests.py | 58 ++++++++++++++++++++++++++++++++++---- rdf_tests.py | 5 +--- sparql_tests.py | 37 ++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 10 deletions(-) diff --git a/TestConstants.py b/TestConstants.py index c5359fb..43412fe 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -20,6 +20,7 @@ FCR_VERSIONS = "fcr:versions" FCR_FIXITY = "fcr:fixity" FCR_TX = "fcr:tx" +FCR_TOMBSTONE = "fcr:tombstone" SERVER_MANAGED = FEDORA_NS + "ServerManaged" INBOUND_REFERENCE = FEDORA_NS + "InboundReferences" EMBEDED_RESOURCE = FEDORA_NS + "EmbedResources" diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index c8446d9..21ec628 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -287,6 +287,20 @@ def assertTitleExists(self, expected, location): return self.fail("Did not find expected title \"{0}\" in response".format(expected)) + def assertTypeExists(self, expected, location): + """ Check resource at {location} for the rdf:type {expected} """ + get_headers = { + 'Accept': TestConstants.JSONLD_MIMETYPE + } + response = self.do_get(location, headers=get_headers) + body = response.content.decode('UTF-8') + json_body = json.loads(body) + found_title = pyjq.all('.[] | ."@type" | .[]', json_body) + for title in found_title: + if title == expected: + return + self.fail("Did not find expected type \"{0}\" in response".format(expected)) + def assertLinkHeaderExists(self, response, rel, expected=None): """ Check for the existence of a link header and possibly match the URI """ link_headers = self.get_link_headers(response) @@ -324,6 +338,13 @@ def checkValue(self, expected, received): self.assertEqual(expected, received, "Did not get expected value") FedoraTests.log(" Passed {0} == {0}".format(received)) + def createBasicContainer(self, parent_location): + """ Make a simple basic container with some RDF content. """ + headers = { + 'Content-type': TestConstants.TURTLE_MIMETYPE + } + return self.do_post(parent_location, headers, TestConstants.OBJECT_TTL) + def Test(func): """ Decorator for isolating test functions """ diff --git a/basic_interaction_tests.py b/basic_interaction_tests.py index ea05737..d05e08e 100644 --- a/basic_interaction_tests.py +++ b/basic_interaction_tests.py @@ -5,6 +5,7 @@ import os import pyjq import json +import uuid @register_tests @@ -31,6 +32,54 @@ def createTestResource(self, type, files=None): self.assertIn(type, type_headers['type'], "Did not find link header for {}".format(type)) return location + @Test + def aTestMissingResource(self): + fake_id = str(uuid.uuid4()) + r = self.do_get(self.getFedoraBase() + "/" + fake_id) + self.assertEqual(404, r.status_code, "Did not get expected response") + + @Test + def testDeleteAResource(self): + self.log("Create container") + r = self.do_post(self.getBaseUri()) + container_location = self.get_location(r) + self.assertEqual(201, r.status_code, "Did not get expected response") + + self.log("Check container exists") + r = self.do_head(container_location) + self.assertEqual(200, r.status_code, "Did not get expected response") + r = self.do_get(container_location) + self.assertEqual(200, r.status_code, "Did not get expected response") + + self.log("Delete the container") + r = self.do_delete(container_location) + self.assertEqual(204, r.status_code, "Did not get expected response") + + self.log("Check container doesn't exists") + r = self.do_head(container_location) + self.assertEqual(410, r.status_code, "Did not get expected response") + r = self.do_get(container_location) + self.assertEqual(410, r.status_code, "Did not get expected response") + + self.log("Try to put to location held by tombstone") + r = self.do_put(container_location) + self.assertEqual(410, r.status_code, "Did not get expected response") + + self.log("Delete the tombstone") + r = self.do_delete(container_location + "/" + TestConstants.FCR_TOMBSTONE) + self.assertEqual(204, r.status_code, "Did not get expected response") + + self.log("Check container doesn't exists") + r = self.do_head(container_location) + self.assertEqual(404, r.status_code, "Did not get expected response") + r = self.do_get(container_location) + self.assertEqual(404, r.status_code, "Did not get expected response") + + self.log("Try to put to location again") + r = self.do_put(container_location) + self.assertEqual(201, r.status_code, "Did not get expected response") + + @Test def testBasicContainer(self): self.createTestResource(TestConstants.LDP_BASIC) @@ -71,15 +120,12 @@ def testLdpContainer(self): @Test def doNestedTests(self): self.log("Create a container") - headers = { - 'Content-type': 'text/turtle' - } - r = self.do_post(self.getBaseUri(), headers=headers, body=TestConstants.OBJECT_TTL) + r = self.createBasicContainer(self.getBaseUri()) self.assertEqual(201, r.status_code, "Did not get expected status code") location = self.get_location(r) self.log("Create a container in a container") - r = self.do_post(location, headers=headers, body=TestConstants.OBJECT_TTL) + r = self.createBasicContainer(location) self.assertEqual(201, r.status_code, "Did not get expected status code") main_child1 = self.get_location(r) @@ -94,7 +140,7 @@ def doNestedTests(self): binary_location = self.get_location(r) self.log("Create a second child in the top container") - r = self.do_post(location, headers=headers, body=TestConstants.OBJECT_TTL) + r = self.createBasicContainer(location) self.assertEqual(201, r.status_code, "Did not get expected status code") main_child2 = self.get_location(r) diff --git a/rdf_tests.py b/rdf_tests.py index 3e9959d..196fdcb 100644 --- a/rdf_tests.py +++ b/rdf_tests.py @@ -21,10 +21,7 @@ class FedoraRdfTests(FedoraTests): @Test def testRdfSerialization(self): self.log("Put new resource.") - headers = { - "Content-type": "text/turtle" - } - r = self.do_post(self.getBaseUri(), headers=headers, body=TestConstants.OBJECT_TTL) + r = self.createBasicContainer(self.getBaseUri()) self.assertEqual(201, r.status_code, "Did not create new object") location = self.get_location(r) diff --git a/sparql_tests.py b/sparql_tests.py index 35d297e..0105c67 100644 --- a/sparql_tests.py +++ b/sparql_tests.py @@ -83,3 +83,40 @@ def doUnicodeSparql(self): r = self.do_patch(location, headers=headers, body=sparql) self.assertEqual(204, r.status_code, "Did not get expected response code") self.assertTitleExists("Die von Blumenbach gegründete anthropologische Sammlung der Universität", location) + + @Test + def doAddType(self): + self.log("Create a container") + r = self.do_post(self.getBaseUri()) + self.assertEqual(201, r.status_code, "Did not get expected response code") + location = self.get_location(r) + + sparql = "PREFIX rdf: " \ + "PREFIX ldp: " \ + "PREFIX example: " \ + "INSERT DATA { <> rdf:type example:type }" + self.log("Patching with our own type") + headers = { + 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + } + r = self.do_patch(location, headers=headers, body=sparql) + self.assertEqual(204, r.status_code, "Did not get expected response code") + self.assertTypeExists("http://www.example.org/ns#type", location) + + @Test + def doAddRestrictedType(self): + self.log("Create a container") + r = self.do_post(self.getBaseUri()) + self.assertEqual(201, r.status_code, "Did not get expected response code") + location = self.get_location(r) + + sparql = "PREFIX rdf: " \ + "PREFIX ldp: " \ + "PREFIX example: " \ + "INSERT DATA {{ <> rdf:type <{0}> }}".format(TestConstants.LDP_DIRECT) + self.log("Patching with our own type") + headers = { + 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + } + r = self.do_patch(location, headers=headers, body=sparql) + self.assertwEqual(409, r.status_code, "Did not get expected response code") From 824ee581d0d6c239da1b4bd0481c10e5fa21e6a0 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 27 May 2022 12:03:15 -0500 Subject: [PATCH 12/20] Lots of updates for Fedora 6 and new simple search tool/tests --- FedoraSearchTool.py | 126 +++++++++++++++ TestConstants.py | 26 ++- abstract_fedora_tests.py | 26 ++- authz_tests.py | 322 +++++++++++++++++++++++++++---------- basic_interaction_tests.py | 177 +++++++++++++++----- fixity_tests.py | 30 ++-- indirect_tests.py | 128 +++++++++++++++ rdf_tests.py | 56 +++++++ requirements.txt | 1 + sparql_tests.py | 119 ++++++++++++-- ssearch_tests.py | 71 ++++++++ testrunner.py | 9 +- transaction_tests.py | 252 ++++++++++++++++++++++++----- version_tests.py | 277 ++++++++++++++++++------------- 14 files changed, 1317 insertions(+), 303 deletions(-) create mode 100644 FedoraSearchTool.py create mode 100644 ssearch_tests.py diff --git a/FedoraSearchTool.py b/FedoraSearchTool.py new file mode 100644 index 0000000..4b45a7f --- /dev/null +++ b/FedoraSearchTool.py @@ -0,0 +1,126 @@ +#!/bin/env python + +import TestConstants as TC +import requests + + +class FedoraSearchTool(object): + + max_results = None + offset = None + order_by = None + order = None + baseUri = None + fields = list() + conditions = list() + authz = None + + def __init__(self, base, authz): + self.authz = authz + self.baseUri = base + + @staticmethod + def create(base_uri, calling_class): + """ + Create a FedoraSearchTool instance + :param base_uri: The base uri of the fedora instance. + :param calling_class: The test class this is used in to get the authorization + :return: FedoraSearchTool + """ + return FedoraSearchTool(base_uri, calling_class.get_auth(True)) + + def clear(self): + """ + Clear all search parameters. + :return: void + """ + self.fields = list() + self.conditions = list() + self.max_results = None + self.offset = None + self.order_by = None + self.order = None + + def add_field(self, field): + """ + Add a field to return in the search results. + :param field: The field + :return: void + """ + if field not in self.fields: + self.fields.append(field) + + def add_condition(self, field, operator, value): + """ + Add a condition to the search + :param field: The field to search against. + :param operator: The operator to use in the search. + :param value: The value to compare (operator) against the field + :return: void + """ + new_condition = field + operator + value + if new_condition not in self.conditions: + self.conditions.append(new_condition) + + def set_max_results(self, count): + """ + Set the max results. + :param count: Max results. + :return: void + """ + self.max_results = count + + def set_order_by(self, field): + """ + Set the field to order by. + :param field: The field. + :return: void + """ + self.order_by = field + + def set_order(self, order): + """ + Set the order to use with the order_by field. + :param order: Order + :return: void + TODO: Should use an enum of some sort. + """ + self.order = order + + def set_offset(self, offset): + """ + Set the offset. + :param offset: The offset. + :return: void + """ + self.offset = offset + + def do_query(self): + """ + Perform the search. + :return: Response object. + """ + parameters = {} + for condition in self.conditions: + if 'condition' in parameters: + try: + parameters['condition'].append(condition) + except AttributeError: + tmp = parameters['condition'] + parameters['condition'] = [tmp] + parameters['condition'].append(condition) + else: + parameters['condition'] = condition + + if len(self.fields) > 0: + parameters['fields'] = ",".join(self.fields) + if self.max_results is not None: + parameters['max_results'] = self.max_results + if self.offset is not None: + parameters['offset'] = self.offset + if self.order_by is not None: + parameters['order_by'] = self.order_by + if self.order is not None: + parameters['order'] = self.order + + return requests.get(self.baseUri + "/" + TC.FCR_SEARCH, auth=self.authz, params=parameters) diff --git a/TestConstants.py b/TestConstants.py index 43412fe..fd35f02 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -12,18 +12,25 @@ SELECTED_TESTS_PARAM = "selected_tests" SOLR_URL_PARAM = "solrurl" TRIPLESTORE_URL_PARAM = "triplestoreurl" +# API Test Suite things +USER_URL_PREFIX = "http://example.com/" # Via RFC 7231 3.3 PAYLOAD_HEADERS = ['Content-Length', 'Content-Range', 'Trailer', 'Transfer-Encoding'] # Fedora specific constants FEDORA_NS = "http://fedora.info/definitions/v4/repository#" +FEDORA_API_NS = "http://fedora.info/definitions/fcrepo#" FCR_VERSIONS = "fcr:versions" FCR_FIXITY = "fcr:fixity" FCR_TX = "fcr:tx" FCR_TOMBSTONE = "fcr:tombstone" +FCR_ACL = "fcr:acl" +FCR_SEARCH = "fcr:search" +FCR_METADATA = "fcr:metadata" SERVER_MANAGED = FEDORA_NS + "ServerManaged" -INBOUND_REFERENCE = FEDORA_NS + "InboundReferences" +INBOUND_REFERENCE = FEDORA_API_NS + "PreferInboundReferences" EMBEDED_RESOURCE = FEDORA_NS + "EmbedResources" +ATOMIC_ID_HEADER="Atomic-ID" FEDORA_TX_NS = "http://fedora.info/definitions/v4/transaction#" FEDORA_TX_ENDPOINT_REL = FEDORA_TX_NS + "endpoint" @@ -37,6 +44,8 @@ RFC_1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" # General Mime and LDP constants +FEDORA_BINARY = FEDORA_NS + "Binary" + JSONLD_MIMETYPE = "application/ld+json" SPARQL_UPDATE_MIMETYPE = "application/sparql-update" TURTLE_MIMETYPE = "text/turtle" @@ -74,3 +83,18 @@ "@prefix pcdm: ." \ "<> a pcdm:Object ;" \ "dc:title \"{0}\" .".format(PCDM_CONTAINER_TITLE) +# Standard Response codes +CREATED = 201 +NO_CONTENT = 204 +OK = 200 +NOT_FOUND = 404 +CONFLICT = 409 +GONE = 410 +BAD_REQUEST = 400 +METHOD_NOT_ALLOWED = 405 + +# Some standard properties +DC_NAMESPACE = "http://purl.org/dc/elements/1.1/" +RDF_NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +DC_TITLE = "{}title".format(DC_NAMESPACE) +RDF_TYPE = "{}type".format(RDF_NAMESPACE) diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index 21ec628..8229cf7 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -65,6 +65,17 @@ def not_authorized(self): print("Cannot connect the Fedora server, is your configuration correct? {0}".format(baseurl)) quit() + def get_transaction_provider(self): + headers = { + 'Accept': TestConstants.JSONLD_MIMETYPE + } + r = self.do_head(self.getFedoraBase(), headers=headers) + self.assertEqual(200, r.status_code, "Did not get expected response") + link_headers = self.get_link_headers(r) + if TestConstants.FEDORA_TX_ENDPOINT_REL in link_headers.keys(): + return link_headers.get(TestConstants.FEDORA_TX_ENDPOINT_REL)[0] + return None + @staticmethod def create_auth(username, password): """ Create a Basic Auth object using the provided username:password """ @@ -322,6 +333,11 @@ def assertHeaderExists(self, response, name, value=None): self.fail("Did not find expected header value for header {0}, found {1} expected {2}" .format(name, response.headers[name], value)) + def assertContrainedByHeaderExists(self, response): + """ Check for link headers with the constrainedby rel type """ + self.assertLinkHeaderExists(response, "http://www.w3.org/ns/ldp#constrainedBy") + + @staticmethod def log(message): print(message) @@ -335,7 +351,7 @@ def checkResponse(self, expected, response): self.checkValue(expected, response.status_code) def checkValue(self, expected, received): - self.assertEqual(expected, received, "Did not get expected value") + self.assertEqual(expected, received, "Expected {} but received {}".format(expected, received)) FedoraTests.log(" Passed {0} == {0}".format(received)) def createBasicContainer(self, parent_location): @@ -345,6 +361,14 @@ def createBasicContainer(self, parent_location): } return self.do_post(parent_location, headers, TestConstants.OBJECT_TTL) + def verifyGet(self, uri, admin=True): + r = self.do_get(uri, admin=admin) + self.checkResponse(TestConstants.OK, r) + + def verifyGone(self, uri, admin=True): + r = self.do_get(uri, admin=admin) + self.checkResponse(TestConstants.GONE, r) + def Test(func): """ Decorator for isolating test functions """ diff --git a/authz_tests.py b/authz_tests.py index d0ada43..7b31bd6 100644 --- a/authz_tests.py +++ b/authz_tests.py @@ -1,6 +1,6 @@ #!/bin/env python -import TestConstants +import TestConstants as TC from abstract_fedora_tests import FedoraTests, register_tests, Test import random import uuid @@ -8,7 +8,6 @@ @register_tests class FedoraAuthzTests(FedoraTests): - # Create test objects all inside here for easy of review CONTAINER = "/test_authz" @@ -61,7 +60,7 @@ def doAuthTests(self): "PREFIX rdf: " \ "INSERT { <> rdf:type pcdm:Object } WHERE { }" headers = { - 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(cover_location, headers=headers, body=sparql) self.checkResponse(204, r) @@ -74,7 +73,7 @@ def doAuthTests(self): headers = { 'Content-type': 'text/turtle' } - body = self.COVER_ACL.format(cover_location, self.config[TestConstants.USER_NAME_PARAM]) + body = self.COVER_ACL.format(cover_location, self.config[TC.USER_NAME_PARAM]) r = self.do_put(cover_acl, headers=headers, body=body) self.checkResponse(201, r) @@ -92,29 +91,29 @@ def doAuthTests(self): r = self.do_get(files_location, admin=None) self.checkResponse(401, r) - self.log("{0} can access \"cover\"".format(self.config[TestConstants.ADMIN_USER_PARAM])) + self.log("{0} can access \"cover\"".format(self.config[TC.ADMIN_USER_PARAM])) r = self.do_get(cover_location) self.checkResponse(200, r) - self.log("{0} can access \"cover/files\"".format(self.config[TestConstants.ADMIN_USER_PARAM])) + self.log("{0} can access \"cover/files\"".format(self.config[TC.ADMIN_USER_PARAM])) r = self.do_get(files_location) self.checkResponse(200, r) - self.log("{0} can access \"cover\"".format(self.config[TestConstants.USER_NAME_PARAM])) + self.log("{0} can access \"cover\"".format(self.config[TC.USER_NAME_PARAM])) r = self.do_get(cover_location, admin=False) self.checkResponse(200, r) - self.log("{0} can access \"cover/files\"".format(self.config[TestConstants.USER_NAME_PARAM])) + self.log("{0} can access \"cover/files\"".format(self.config[TC.USER_NAME_PARAM])) r = self.do_get(files_location, admin=False) self.checkResponse(200, r) - auth = self.create_auth(self.config[TestConstants.USER2_NAME_PARAM], - self.config[TestConstants.USER2_PASS_PARAM]) - self.log("{0} can't access \"cover\"".format(self.config[TestConstants.USER2_NAME_PARAM])) + auth = self.create_auth(self.config[TC.USER2_NAME_PARAM], + self.config[TC.USER2_PASS_PARAM]) + self.log("{0} can't access \"cover\"".format(self.config[TC.USER2_NAME_PARAM])) r = self.do_get(cover_location, admin=auth) self.checkResponse(403, r) - self.log("{0} can't access \"cover/files\"".format(self.config[TestConstants.USER2_NAME_PARAM])) + self.log("{0} can't access \"cover/files\"".format(self.config[TC.USER2_NAME_PARAM])) r = self.do_get(files_location, admin=auth) self.checkResponse(403, r) @@ -122,19 +121,19 @@ def doAuthTests(self): r = self.do_get(files_acl) self.checkResponse(404, r) - self.log("PUT Acl to \"cover/files\" to allow access for {0}".format(self.config[TestConstants.USER2_NAME_PARAM])) + self.log("PUT Acl to \"cover/files\" to allow access for {0}".format(self.config[TC.USER2_NAME_PARAM])) headers = { 'Content-type': 'text/turtle' } - body = self.FILES_ACL.format(files_location, self.config[TestConstants.USER2_NAME_PARAM]) + body = self.FILES_ACL.format(files_location, self.config[TC.USER2_NAME_PARAM]) r = self.do_put(files_acl, headers=headers, body=body) self.checkResponse(201, r) - self.log("{0} can't access \"cover\"".format(self.config[TestConstants.USER2_NAME_PARAM])) + self.log("{0} can't access \"cover\"".format(self.config[TC.USER2_NAME_PARAM])) r = self.do_get(cover_location, admin=auth) self.checkResponse(403, r) - self.log("{0} can access \"cover/files\"".format(self.config[TestConstants.USER2_NAME_PARAM])) + self.log("{0} can access \"cover/files\"".format(self.config[TC.USER2_NAME_PARAM])) r = self.do_get(files_location, admin=auth) self.checkResponse(200, r) @@ -155,64 +154,68 @@ def doDirectIndirectAuthTests(self): write_acl = FedoraAuthzTests.getAclUri(r) self.log("Make sure the /target resource is readonly") - target_ttl = "@prefix acl: <{2}> .\n"\ + target_ttl = "@prefix acl: <{2}> .\n" \ "<#readauthz> a acl:Authorization ;\n" \ " acl:agent \"{0}\" ;\n" \ " acl:mode acl:Read ;\n" \ - " acl:accessTo <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], target_location, - TestConstants.ACL_NS) + " acl:accessTo <{1}> .\n".format(self.config[TC.USER_NAME_PARAM], target_location, + TC.ACL_NS) headers = { 'Content-type': 'text/turtle' } r = self.do_put(target_acl, headers=headers, body=target_ttl) self.checkResponse(201, r) - self.log("Make sure the write resource is writable by \"{0}\"".format(self.config[TestConstants.USER_NAME_PARAM])) + self.log("Make sure the write resource is writable by \"{0}\"".format(self.config[TC.USER_NAME_PARAM])) write_ttl = "@prefix acl: <{2}> .\n" \ "<#writeauth> a acl:Authorization ;\n" \ " acl:agent \"{0}\" ;\n" \ " acl:mode acl:Read, acl:Write ;\n" \ " acl:accessTo <{1}> ;\n" \ - " acl:default <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], write_location, - TestConstants.ACL_NS) + " acl:default <{1}> .\n".format(self.config[TC.USER_NAME_PARAM], write_location, + TC.ACL_NS) r = self.do_put(write_acl, headers=headers, body=write_ttl) self.checkResponse(201, r) - self.log("Verify that \"{0}\" can create a simple resource under write resource (POST)".format(self.config[TestConstants.USER_NAME_PARAM])) + self.log("Verify that \"{0}\" can create a simple resource under write resource (POST)".format( + self.config[TC.USER_NAME_PARAM])) r = self.do_post(write_location, admin=False) self.checkResponse(201, r) uuid_value = str(uuid.uuid4()) self.log("Verify that \"{0}\" can create a simple resource under write resource (PUT)".format( - self.config[TestConstants.USER_NAME_PARAM])) + self.config[TC.USER_NAME_PARAM])) r = self.do_put(write_location + "/" + uuid_value, admin=False) self.checkResponse(201, r) - self.log("Verify that \"{0}\" CANNOT create a resource under target resource".format(self.config[TestConstants.USER_NAME_PARAM])) + self.log("Verify that \"{0}\" CANNOT create a resource under target resource".format( + self.config[TC.USER_NAME_PARAM])) r = self.do_post(target_location, admin=False) self.checkResponse(403, r) - self.log("Verify that \"{0}\" CANNOT create direct or indirect containers that reference target resources".format(self.config[TestConstants.USER_NAME_PARAM])) + self.log( + "Verify that \"{0}\" CANNOT create direct or indirect containers that reference target resources".format( + self.config[TC.USER_NAME_PARAM])) headers = { 'Content-type': 'text/turtle', - 'Link': self.make_type(TestConstants.LDP_DIRECT) + 'Link': self.make_type(TC.LDP_DIRECT) } direct_ttl = "@prefix ldp: <{0}> .\n" \ "@prefix test: .\n" \ "<> ldp:membershipResource <{1}> ;\n" \ - "ldp:hasMemberRelation test:predicateToCreate .\n".format(TestConstants.LDP_NS, target_location) + "ldp:hasMemberRelation test:predicateToCreate .\n".format(TC.LDP_NS, target_location) r = self.do_post(write_location, headers=headers, body=direct_ttl, admin=False) self.checkResponse(403, r) headers = { 'Content-type': 'text/turtle', - 'Link': self.make_type(TestConstants.LDP_INDIRECT) + 'Link': self.make_type(TC.LDP_INDIRECT) } indirect_ttl = "@prefix ldp: <{0}> .\n" \ "@prefix test: .\n" \ "<> ldp:insertedContentRelation test:something ;\n" \ "ldp:membershipResource <{1}> ;\n" \ - "ldp:hasMemberRelation test:predicateToCreate .\n".format(TestConstants.LDP_NS, target_location) + "ldp:hasMemberRelation test:predicateToCreate .\n".format(TC.LDP_NS, target_location) r = self.do_post(write_location, headers=headers, body=indirect_ttl, admin=False) self.checkResponse(403, r) @@ -225,19 +228,20 @@ def doDirectIndirectAuthTests(self): indirect_location = self.get_location(r) self.log("Attempt to verify that \"{0}\" can not actually create relationships on the readonly resource via " \ - "direct or indirect container".format(self.config[TestConstants.USER_NAME_PARAM])) + "direct or indirect container".format(self.config[TC.USER_NAME_PARAM])) r = self.do_post(direct_location, admin=False) self.assertEqual(403, r.status_code, "Did not get expected status code") r = self.do_post(indirect_location, admin=False) self.assertEqual(403, r.status_code, "Did not get expected status code") - self.log("Verify that \"{0}\" can still create a simple resource under write resource (POST)".format(self.config[TestConstants.USER_NAME_PARAM])) + self.log("Verify that \"{0}\" can still create a simple resource under write resource (POST)".format( + self.config[TC.USER_NAME_PARAM])) r = self.do_post(write_location, admin=False) self.checkResponse(201, r) uuid_value = str(uuid.uuid4()) self.log("Verify that \"{0}\" can still create a simple resource under write resource (PUT)".format( - self.config[TestConstants.USER_NAME_PARAM])) + self.config[TC.USER_NAME_PARAM])) r = self.do_put(write_location + "/" + uuid_value, admin=False) self.checkResponse(201, r) @@ -259,10 +263,10 @@ def multipleAuthzCreatePermissiveSet(self): "<#readwrite> a acl:Authorization ;\n" \ " acl:agent \"{0}\" ;\n" \ " acl:mode acl:Write ;\n" \ - " acl:accessTo <{1}> .\n".format(self.config[TestConstants.USER_NAME_PARAM], target_location, - TestConstants.ACL_NS) + " acl:accessTo <{1}> .\n".format(self.config[TC.USER_NAME_PARAM], target_location, + TC.ACL_NS) headers = { - 'Content-type': TestConstants.TURTLE_MIMETYPE + 'Content-type': TC.TURTLE_MIMETYPE } self.log("Add ACL with one read and one write authz for the same URI.") @@ -275,9 +279,9 @@ def multipleAuthzCreatePermissiveSet(self): self.log("Check we can write") headers = { - 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } - body = "prefix dc: <{0}> INSERT {{ <> dc:title \"A new title\" }} WHERE {{}}".format(TestConstants.PURL_NS) + body = "prefix dc: <{0}> INSERT {{ <> dc:title \"A new title\" }} WHERE {{}}".format(TC.PURL_NS) r = self.do_patch(target_location, headers=headers, body=body) self.checkResponse(204, r) @@ -288,7 +292,7 @@ def testAllThingsPointTogether(self): self.log("Create a target binary") headers = { 'Content-type': 'text/plain', - 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE) + 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE) } r = self.do_post(headers=headers, body="this is a test payload") self.checkResponse(201, r) @@ -321,12 +325,6 @@ def testAllThingsPointTogether(self): self.checkResponse(201, r) memento_location = self.get_location(r) - self.log("Check memento's acl link header is correct") - r = self.do_get(memento_location) - self.checkResponse(200, r) - acl_location = FedoraAuthzTests.getAclUri(r) - self.checkValue(expected_acl, acl_location) - self.log("Check binary description timemap's acl link header is correct.") binary_metadata_versions = binary_description + "/fcr:versions" r = self.do_get(binary_metadata_versions) @@ -334,27 +332,14 @@ def testAllThingsPointTogether(self): acl_location = FedoraAuthzTests.getAclUri(r) self.checkValue(expected_acl, acl_location) - # metadata version is same datetime, so create it. - metadata_memento_location = binary_metadata_versions + memento_location[memento_location.rfind('/'):] - - self.log("Check metadata memento's acl link header is correct") - r = self.do_get(metadata_memento_location) - self.checkResponse(200, r) - acl_location = FedoraAuthzTests.getAclUri(r) - self.checkValue(expected_acl, acl_location) - - self.log("Try to get the ACL") - r = self.do_get(acl_location) - self.checkResponse(404, r) - @Test - def binaryAndMetadataShareACL(self): + def doBinaryAndMetadataShareACL(self): self.verifyAuthEnabled() self.log("Create a target binary") headers = { 'Content-type': 'text/plain', - 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE) + 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE) } r = self.do_post(headers=headers, body="this is a test payload") self.checkResponse(201, r) @@ -367,10 +352,10 @@ def binaryAndMetadataShareACL(self): "<#binary> a acl:Authorization ;\n" \ " acl:mode acl:Write ;\n" \ " acl:accessTo <{1}> ;\n" \ - " acl:agent \"{2}\" .\n".format(TestConstants.ACL_NS, binary_location, - self.config[TestConstants.USER_NAME_PARAM]) + " acl:agent \"{2}\" .\n".format(TC.ACL_NS, binary_location, + self.config[TC.USER_NAME_PARAM]) headers = { - 'Content-type': TestConstants.TURTLE_MIMETYPE + 'Content-type': TC.TURTLE_MIMETYPE } r = self.do_put(acl_location, headers=headers, body=binary_acl) self.checkResponse(201, r) @@ -391,9 +376,9 @@ def binaryAndMetadataShareACL(self): self.checkResponse(403, r) self.log("Try to patch metadata") - patch_body = "prefix dc: <{0}> INSERT DATA {{ <> dc:title \"Updated title\"}}".format(TestConstants.PURL_NS) + patch_body = "prefix dc: <{0}> INSERT DATA {{ <> dc:title \"Updated title\"}}".format(TC.PURL_NS) headers = { - 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(binary_description, patch_body, headers=headers, admin=False) self.checkResponse(204, r) @@ -407,29 +392,29 @@ def testContainerWithAccessToClass(self): r = self.do_post() location = self.get_location(r) acl_location = self.getAclUri(r) - versions_location = location + "/" + TestConstants.FCR_VERSIONS + versions_location = location + "/" + TC.FCR_VERSIONS full_acl = "@prefix acl: <{0}> .\n" \ - "@prefix fedora: <{1}> .\n" \ - "@prefix memento: <{2}> .\n" \ - "<#container> a acl:Authorization ;\n" \ - " acl:mode acl:Read ;\n" \ - " acl:accessTo <{3}> ;\n" \ - " acl:agent \"{4}\" .\n" \ - "<#timemap> a acl:Authorization ;\n" \ - " acl:mode acl:Read ;\n" \ - " acl:accessToClass fedora:TimeMap ;\n" \ - " acl:default <{3}> ;\n" \ - " acl:agent \"{4}\" .\n" \ - "<#memento> a acl:Authorization ;\n" \ - " acl:mode acl:Read ;\n" \ - " acl:accessToClass memento:Memento ;\n" \ - " acl:default <{3}> ;\n" \ - " acl:agent \"{5}\" .\n".format(TestConstants.ACL_NS, TestConstants.FEDORA_NS, TestConstants.MEMENTO_NS, - location, self.config[TestConstants.USER_NAME_PARAM], - self.config[TestConstants.ADMIN_USER_PARAM]) + "@prefix fedora: <{1}> .\n" \ + "@prefix memento: <{2}> .\n" \ + "<#container> a acl:Authorization ;\n" \ + " acl:mode acl:Read ;\n" \ + " acl:accessTo <{3}> ;\n" \ + " acl:agent \"{4}\" .\n" \ + "<#timemap> a acl:Authorization ;\n" \ + " acl:mode acl:Read ;\n" \ + " acl:accessToClass fedora:TimeMap ;\n" \ + " acl:default <{3}> ;\n" \ + " acl:agent \"{4}\" .\n" \ + "<#memento> a acl:Authorization ;\n" \ + " acl:mode acl:Read ;\n" \ + " acl:accessToClass memento:Memento ;\n" \ + " acl:default <{3}> ;\n" \ + " acl:agent \"{5}\" .\n".format(TC.ACL_NS, TC.FEDORA_NS, TC.MEMENTO_NS, + location, self.config[TC.USER_NAME_PARAM], + self.config[TC.ADMIN_USER_PARAM]) turtle_headers = { - 'Content-type': TestConstants.TURTLE_MIMETYPE + 'Content-type': TC.TURTLE_MIMETYPE } user2 = self.create_user2_auth() @@ -470,3 +455,170 @@ def testContainerWithAccessToClass(self): self.log("Try to get memento as test user 2") r = self.do_get(memento_location, admin=user2) self.checkResponse(403, r) + + @Test + def testPermissionsDoNotExtendInTx(self): + self.verifyAuthEnabled() + self.log("Create a container") + r = self.do_post() + location = self.get_location(r) + acl_location = self.getAclUri(r) + headers = { + 'Content-type': TC.TURTLE_MIMETYPE + } + readwriteString = "@prefix acl: .\n" \ + "<#readauthz> a acl:Authorization ;\n" \ + " acl:agent \"{0}\" ;\n" \ + " acl:mode acl:Read, acl:Write ;\n" \ + " acl:accessTo <{1}> .".format(self.config[TC.USER_NAME_PARAM], location) + + r = self.do_put(acl_location, headers=headers, body=readwriteString) + self.checkResponse(201, r) + # Test that user28 can read target resource. + r = self.do_get(location, admin=False) + self.checkResponse(200, r) + # Test that user28 can patch target resource. + patchString = "prefix dc: INSERT { <> dc:title " \ + "\"new title\" } WHERE {}" + patch_headers = { + 'Content-type': 'application/sparql-update' + } + r = self.do_patch(location, headers=patch_headers, body=patchString, admin=False) + self.checkResponse(204, r) + # Test that user28 can post to target resource. + r = self.do_post(location, admin=False) + self.checkResponse(201, r) + childResource = self.get_location(r) + # Test that user28 cannot patch the child resource(ACL is not acl: default). + r = self.do_patch(childResource, headers=patch_headers, body=patchString, admin=False) + self.checkResponse(403, r) + # Test that user28 cannot post to a child resource. + r = self.do_post(childResource, admin=False) + self.checkResponse(403, r) + # Test another user cannot access the target resource. + r = self.do_get(location, admin=self.create_user2_auth()) + self.checkResponse(403, r) + # Get the transaction endpoint. + transactionEndpoint = self.get_transaction_provider() + # Create a transaction. + r = self.do_post(transactionEndpoint) + self.checkResponse(201, r) + transactionId = self.get_location(r) + self.log("transaction ID {0}".format(transactionId)) + # Test user28 can post to target resource in a transaction. + txHeaders = { + 'Atomic-ID': transactionId + } + r = self.do_post(location, headers=txHeaders, admin=False) + self.checkResponse(201, r) + txChild = self.get_location(r) + # Test user28 cannot post to the child in a transaction. + r = self.do_post(txChild, headers=txHeaders, admin=False) + self.checkResponse(403, r) + + @Test + def testGroupAuth(self): + self.log("THIS TEST REQUIRES FEDORA TO HAVE THE TESTSUITE WEBID PREFIX") + agentGroup = "@prefix acl: . " \ + "@prefix vcard: . " \ + "<> a vcard:Group;" \ + "vcard:hasMember <{}{}>.".format(TC.USER_URL_PREFIX, + self.config[TC.USER_NAME_PARAM]) + head = { + 'Content-type': TC.TURTLE_MIMETYPE + } + r = self.do_post(headers=head, body=agentGroup) + self.checkResponse(201, r) + group_location = self.get_location(r) + + r = self.do_post() + self.checkResponse(201, r) + target_location = self.get_location(r) + + acl = "@prefix acl: ." \ + "@prefix foaf: ." \ + "<#authorization> a acl:Authorization; " \ + "acl:accessTo <{}>;" \ + "acl:mode acl:Read, acl:Write;" \ + "acl:agentGroup <{}>.".format(target_location, group_location) + + r = self.do_put(target_location + "/" + TC.FCR_ACL, headers=head, body=acl) + self.checkResponse(201, r) + + r = self.do_get(target_location, admin=False) + self.checkResponse(200, r) + + @Test + def testControlOnlyPut(self): + r = self.do_post() + self.checkResponse(201, r) + resource_uri = self.get_location(r) + + control_acl = "@prefix acl: .\n" \ + "@prefix foaf: .\n" \ + "<#restricted> a acl:Authorization ;\n" \ + "acl:agent \"{0}\" ;\n" \ + "acl:mode acl:Control;\n" \ + "acl:default <{1}> ;\n" \ + "acl:accessTo <{1}> .".format(self.config[TC.USER_NAME_PARAM], resource_uri) + head = { + 'Content-type': TC.TURTLE_MIMETYPE + } + r = self.do_put(resource_uri + "/" + TC.FCR_ACL, headers=head, body=control_acl) + self.checkResponse(201, r) + + # Verify that testuser can not read the resource + r = self.do_get(resource_uri, admin=False) + self.checkResponse(403, r) + + new_control_acl = "@prefix acl: .\n" \ + "@prefix foaf: .\n" \ + " <#openaccess> a acl:Authorization ;" \ + " acl:mode acl:Read ;\n" \ + " acl:agentClass foaf:Agent ; \n" \ + " acl:accessTo <{}> .".format(resource_uri) + # Update ACL as testuser + r = self.do_put(resource_uri + "/" + TC.FCR_ACL, headers=head, body=new_control_acl, admin=False) + self.checkResponse(204, r) + + # Check that now testuser can read + r = self.do_get(resource_uri, admin=False) + self.checkResponse(200, r) + + @Test + def testCanAccessToAndAccessToClass(self): + r = self.do_post() + self.checkResponse(201, r) + resource_uri = self.get_location(r) + + acl = "@prefix acl: .\n" \ + "@prefix foaf: .\n" \ + "@prefix pcdm: .\n" \ + " <#openaccess> a acl:Authorization ;" \ + " acl:mode acl:Read ;\n" \ + " acl:agentClass foaf:Agent ; \n" \ + " acl:accessTo <{}> ; \n" \ + " acl:accessToClass pcdm:Object .".format(resource_uri) + + headers = { + 'Content-type': TC.TURTLE_MIMETYPE + } + + r = self.do_put(resource_uri + "/" + TC.FCR_ACL, headers=headers, body=acl) + self.checkResponse(400, r) + self.assertContrainedByHeaderExists(r) + + @Test + def testPutInvalidAcl(self): + self.log("Create a mock container to use as the ACL") + r = self.do_post() + self.checkResponse(201, r) + resource_uri = self.get_location(r) + + headers = { + 'Link': "<{}>; rel=\"acl\"".format(resource_uri) + } + # Make a new resource trying to define the location of the ACL. + r = self.do_post(headers=headers) + self.checkResponse(400, r) + self.assertContrainedByHeaderExists(r) diff --git a/basic_interaction_tests.py b/basic_interaction_tests.py index d05e08e..c432aed 100644 --- a/basic_interaction_tests.py +++ b/basic_interaction_tests.py @@ -1,6 +1,6 @@ #!/bin/env python -import TestConstants +import TestConstants as TC from abstract_fedora_tests import FedoraTests, register_tests, Test import os import pyjq @@ -66,7 +66,7 @@ def testDeleteAResource(self): self.assertEqual(410, r.status_code, "Did not get expected response") self.log("Delete the tombstone") - r = self.do_delete(container_location + "/" + TestConstants.FCR_TOMBSTONE) + r = self.do_delete(container_location + "/" + TC.FCR_TOMBSTONE) self.assertEqual(204, r.status_code, "Did not get expected response") self.log("Check container doesn't exists") @@ -82,25 +82,25 @@ def testDeleteAResource(self): @Test def testBasicContainer(self): - self.createTestResource(TestConstants.LDP_BASIC) + self.createTestResource(TC.LDP_BASIC) @Test def testDirectContainer(self): - self.createTestResource(TestConstants.LDP_DIRECT) + self.createTestResource(TC.LDP_DIRECT) @Test def testIndirectContainer(self): - self.createTestResource(TestConstants.LDP_INDIRECT) + self.createTestResource(TC.LDP_INDIRECT) @Test def testNonRdfSource(self): testfiles = {'files': ('testdata.csv', 'this,is,some,data\n')} - self.createTestResource(TestConstants.LDP_NON_RDF_SOURCE, files=testfiles) + self.createTestResource(TC.LDP_NON_RDF_SOURCE, files=testfiles) @Test def testLdpResource(self): """ We don't allow you to create a ldp:Resource so this returns 400 Bad Request """ - link_type = self.make_type(TestConstants.LDP_RESOURCE) + link_type = self.make_type(TC.LDP_RESOURCE) headers = { 'Link': link_type } @@ -110,7 +110,7 @@ def testLdpResource(self): @Test def testLdpContainer(self): """ We don't allow you to create a ldp:Container so this returns 400 Bad Request """ - link_type = self.make_type(TestConstants.LDP_CONTAINER) + link_type = self.make_type(TC.LDP_CONTAINER) headers = { 'Link': link_type } @@ -146,7 +146,7 @@ def doNestedTests(self): self.log("Verify containment") headers = { - 'Accept': TestConstants.JSONLD_MIMETYPE + 'Accept': TC.JSONLD_MIMETYPE } r = self.do_get(location, headers=headers) self.assertEqual(200, r.status_code, "Can't get the container") @@ -186,6 +186,59 @@ def doNestedTests(self): r = self.do_get(location) self.assertEqual(410, r.status_code, "Did not get expected status code") + @Test + def testPurgeContainer(self): + r = self.do_post() + self.checkResponse(TC.CREATED, r) + uri = self.get_location(r) + + self.verifyGet(uri) + + r = self.do_post(uri) + self.checkResponse(TC.CREATED, r) + childUri = self.get_location(r) + + r = self.do_delete(childUri) + self.checkResponse(TC.NO_CONTENT, r) + + r = self.do_get(childUri) + self.checkResponse(TC.GONE, r) + + r = self.do_delete(childUri + "/" + TC.FCR_TOMBSTONE) + self.checkResponse(TC.NO_CONTENT, r) + + r = self.do_get(childUri) + self.checkResponse(TC.NOT_FOUND, r) + + r = self.do_put(childUri) + self.checkResponse(TC.CREATED, r) + + @Test + def testPurgeBinary(self): + headers = { + 'Link': "<{}>; rel=\"type\"".format(TC.LDP_NON_RDF_SOURCE) + } + r = self.do_post(headers=headers, body="some text") + self.checkResponse(TC.CREATED, r) + childUri = self.get_location(r) + + self.verifyGet(childUri) + + r = self.do_delete(childUri) + self.checkResponse(TC.NO_CONTENT, r) + + r = self.do_get(childUri) + self.checkResponse(TC.GONE, r) + + r = self.do_delete(childUri + "/" + TC.FCR_TOMBSTONE) + self.checkResponse(TC.NO_CONTENT, r) + + r = self.do_get(childUri) + self.checkResponse(TC.NOT_FOUND, r) + + r = self.do_put(childUri) + self.checkResponse(TC.CREATED, r) + def changeIxnModels(self, location, starting_model): """ This function uses a created object at {location} with starting type {starting_model}. The below dictionary of tuples works as such @@ -194,39 +247,39 @@ def changeIxnModels(self, location, starting_model): (, ), """ expected_ixn_change = { - TestConstants.LDP_BASIC: [ - (TestConstants.LDP_INDIRECT, 409), - (TestConstants.LDP_DIRECT, 409), - (TestConstants.LDP_NON_RDF_SOURCE, 409), - (TestConstants.LDP_RESOURCE, 400), - (TestConstants.LDP_CONTAINER, 400) + TC.LDP_BASIC: [ + (TC.LDP_INDIRECT, 409), + (TC.LDP_DIRECT, 409), + (TC.LDP_NON_RDF_SOURCE, 409), + (TC.LDP_RESOURCE, 400), + (TC.LDP_CONTAINER, 400) ], - TestConstants.LDP_DIRECT: [ - (TestConstants.LDP_BASIC, 409), - (TestConstants.LDP_INDIRECT, 409), - (TestConstants.LDP_NON_RDF_SOURCE, 409), - (TestConstants.LDP_RESOURCE, 400), - (TestConstants.LDP_CONTAINER, 400) + TC.LDP_DIRECT: [ + (TC.LDP_BASIC, 409), + (TC.LDP_INDIRECT, 409), + (TC.LDP_NON_RDF_SOURCE, 409), + (TC.LDP_RESOURCE, 400), + (TC.LDP_CONTAINER, 400) ], - TestConstants.LDP_INDIRECT: [ - (TestConstants.LDP_BASIC, 409), - (TestConstants.LDP_DIRECT, 409), - (TestConstants.LDP_NON_RDF_SOURCE, 409), - (TestConstants.LDP_RESOURCE, 400), - (TestConstants.LDP_CONTAINER, 400) + TC.LDP_INDIRECT: [ + (TC.LDP_BASIC, 409), + (TC.LDP_DIRECT, 409), + (TC.LDP_NON_RDF_SOURCE, 409), + (TC.LDP_RESOURCE, 400), + (TC.LDP_CONTAINER, 400) ], - TestConstants.LDP_NON_RDF_SOURCE: [ - (TestConstants.LDP_BASIC, 409), - (TestConstants.LDP_DIRECT, 409), - (TestConstants.LDP_INDIRECT, 409), - (TestConstants.LDP_RESOURCE, 400), - (TestConstants.LDP_CONTAINER, 400) + TC.LDP_NON_RDF_SOURCE: [ + (TC.LDP_BASIC, 409), + (TC.LDP_DIRECT, 409), + (TC.LDP_INDIRECT, 409), + (TC.LDP_RESOURCE, 400), + (TC.LDP_CONTAINER, 400) ] } for model, result in expected_ixn_change[starting_model]: self.log("Changing from {0} to {1} expect status {2}".format(starting_model, model, result)) - if model == TestConstants.LDP_NON_RDF_SOURCE: + if model == TC.LDP_NON_RDF_SOURCE: files = {'file': ('testcsvdata.csv', 'this,is,changed,data\nnow,go,away,please\n')} else: files = None @@ -241,18 +294,58 @@ def changeIxnModels(self, location, starting_model): @Test def testChangeIxnModel(self): self.log("Create a basic container") - basic = self.createTestResource(TestConstants.LDP_BASIC) - self.changeIxnModels(basic, TestConstants.LDP_BASIC) + basic = self.createTestResource(TC.LDP_BASIC) + self.changeIxnModels(basic, TC.LDP_BASIC) self.log("Create a direct container") - direct = self.createTestResource(TestConstants.LDP_DIRECT) - self.changeIxnModels(direct, TestConstants.LDP_DIRECT) + direct = self.createTestResource(TC.LDP_DIRECT) + self.changeIxnModels(direct, TC.LDP_DIRECT) self.log("Create a indirect container") - indirect = self.createTestResource(TestConstants.LDP_INDIRECT) - self.changeIxnModels(indirect, TestConstants.LDP_INDIRECT) + indirect = self.createTestResource(TC.LDP_INDIRECT) + self.changeIxnModels(indirect, TC.LDP_INDIRECT) self.log("Create a Non Rdf Source") testfiles = {'files': ('testdata.csv', 'this,is,some,data\n')} - non_rdf = self.createTestResource(TestConstants.LDP_NON_RDF_SOURCE, files=testfiles) - self.changeIxnModels(non_rdf, TestConstants.LDP_NON_RDF_SOURCE) + non_rdf = self.createTestResource(TC.LDP_NON_RDF_SOURCE, files=testfiles) + self.changeIxnModels(non_rdf, TC.LDP_NON_RDF_SOURCE) + + def testBinaryTriples(self): + self.log("Create binary with expected properties") + headers = { + 'Content-type': 'text/plain', + 'Content-Disposition': 'attachment; filename="mytestfile.txt"' + } + r = self.do_post(self.getBaseUri(), headers=headers, body="some sample text") + self.checkResponse(TC.CREATED, r) + + def testChecksum(self): + self.log("Create parent resource") + r = self.do_post(self.getBaseUri()); + self.checkResponse(TC.CREATED, r) + parent_uri = self.get_location(r) + + first_etag = self.getEtag(parent_uri) + + r = self.do_post(parent_uri) + self.checkResponse(TC.CREATED, r) + + second_etag = self.getEtag(parent_uri) + + self.assertNotEqual(first_etag, second_etag) + + for i in range(1, 30): + r = self.do_post(parent_uri, {'Slug': 'child_' + i}) + self.checkResponse(TC.CREATED, r) + + third_etag = self.getEtag(parent_uri) + + + self.assertNotEqual(first_etag, third_etag) + self.assertNotEqual(second_etag, third_etag) + + def getEtag(self, uri): + r = self.do_head(uri) + self.assertTrue(TC.OK, r) + if "ETag" in r.headers: + return r.headers["ETag"] \ No newline at end of file diff --git a/fixity_tests.py b/fixity_tests.py index 4f6e9ed..5c74177 100644 --- a/fixity_tests.py +++ b/fixity_tests.py @@ -14,7 +14,14 @@ class FedoraFixityTests(FedoraTests): CONTAINER = "/test_fixity" # Sha1 fixity result for basic_image.jpg in the resource sub-directory - FIXITY_RESULT = "urn:sha1:dec028a4400b4f7ed80ed1174e65179d6b57a0f2" + FIXITY_RESULT_SHA1 = "dec028a4400b4f7ed80ed1174e65179d6b57a0f2" + + def decode_digest_header(self, header): + digests = dict() + for digest in header.split(","): + (alg, value) = digest.split('=') + digests[alg] = value + return digests @Test def aFixityTest(self): @@ -29,17 +36,18 @@ def aFixityTest(self): self.assertEqual(201, r.status_code, 'Did not create binary') location = self.get_location(r) - fixity_endpoint = location + "/" + TestConstants.FCR_FIXITY self.log("Get a fixity result") headers = { - 'Accept': TestConstants.JSONLD_MIMETYPE + 'Want-Digest': 'sha' } - r = self.do_get(fixity_endpoint, headers=headers) + r = self.do_head(location, headers=headers) self.assertEqual(200, r.status_code, "Can't get the fixity result") - body = r.content.decode('UTF-8').rstrip('\ny') - json_body = json.loads(body) - fixity_id = pyjq.first('.[0]."http://www.loc.gov/premis/rdf/v1#hasFixity"| .[]?."@id"', json_body) - fixity_result = pyjq.first('.[] | select(."@id" == "{0}") | ' - '."http://www.loc.gov/premis/rdf/v1#hasMessageDigest" | .[]?."@id"'.format(fixity_id), - json_body) - self.assertEqual(self.FIXITY_RESULT, fixity_result, "Fixity result was not a match for expected.") + if 'Digest' in r.headers: + fixity_results = self.decode_digest_header(r.headers['Digest']) + if 'sha' in fixity_results.keys(): + self.assertEqual(self.FIXITY_RESULT_SHA1, fixity_results['sha'], + "Fixity result was not a match for expected.") + else: + self.fail("No sha digest returned") + else: + self.fail("No Digest header returned") diff --git a/indirect_tests.py b/indirect_tests.py index 0389327..1a7c6d2 100644 --- a/indirect_tests.py +++ b/indirect_tests.py @@ -4,6 +4,7 @@ from abstract_fedora_tests import FedoraTests, register_tests, Test import json import pyjq +import uuid @register_tests @@ -68,3 +69,130 @@ def doPcdmIndirect(self): if member == pcdm_container_location: found_member = True self.assertTrue(found_member, "Did not find hasMember property") + + @Test + def doAddIndirect(self): + read_only = str(uuid.uuid4()) + headers = { + 'Slug': read_only + } + r = self.do_post(self.getBaseUri(), headers=headers) + read_only_location = self.get_location(r) + self.log("making a read-only resource : {0}".format(read_only_location)) + self.assertEqual(201, r.status_code, "Did not get expected status code") + + turtle_headers = { + 'Content-type': TestConstants.TURTLE_MIMETYPE + } + read_only_acl = "@prefix acl: . \n"\ + "<#readauthz> a acl:Authorization ; \n"\ + "acl:agent \"{0}\" ;\n" \ + "acl:mode acl:Read ;\n" \ + "acl:accessTo <{1}> .".format(self.config[TestConstants.USER_NAME_PARAM], read_only_location) + self.log("adding an acl to the read-only resource") + r = self.do_put(read_only_location + "/" + TestConstants.FCR_ACL, headers=turtle_headers, body=read_only_acl) + self.assertEqual(201, r.status_code, "Did not get expected status code") + + self.log("Do get as testuser") + r = self.do_get(read_only_location, admin=False) + self.assertEqual(200, r.status_code, "Did not get expected status code") + + patch_headers = { + "Content-type": TestConstants.SPARQL_UPDATE_MIMETYPE + } + patch_body = "INSERT DATA { <> \"Changed it\"}" + self.log("Try patch as testuser") + r = self.do_patch(read_only_location, headers=patch_headers, body=patch_body, admin=False) + self.assertEqual(403, r.status_code, "Did not get expected status code") + + writeable = str(uuid.uuid4()) + headers = { + 'Slug': writeable + } + r = self.do_post(self.getBaseUri(), headers=headers) + self.assertEqual(201, r.status_code, "Did not get expected status code") + writeable_location = self.get_location(r) + self.log("create a new writeable resource at {0}".format(writeable_location)) + + writeable_acl = "@prefix acl: .\n" \ + "<#writeauth> a acl:Authorization ;\n" \ + " acl:agent \"{0}\" ;\n" \ + " acl:mode acl:Read, acl:Write ;\n" \ + " acl:accessTo <{1}> ;\n" \ + " acl:default <{1}> .".format(self.config[TestConstants.USER_NAME_PARAM], writeable_location) + self.log("create an ACl on the writeable resource") + r = self.do_put(writeable_location + "/" + TestConstants.FCR_ACL, headers=turtle_headers, body=writeable_acl) + self.assertEqual(201, r.status_code, "Did not get expected status code") + + self.log("Test writing inside the writeable resource as testuser") + r = self.do_post(writeable_location, admin=False) + self.assertEqual(201, r.status_code, "Did not get expected status code") + + indirect_template = "@prefix ldp: .\n" \ + "@prefix example: .\n" \ + "@prefix dc: .\n" \ + "<> ldp:insertedContentRelation ;\n" \ + "ldp:membershipResource <{0}> ;\n" \ + "ldp:hasMemberRelation ;\n" \ + "dc:title \"The indirect container\" ." + indirect_body = indirect_template.format(read_only_location) + headers = { + "Slug": "indirect", + "Content-type": TestConstants.TURTLE_MIMETYPE, + 'Link': self.make_type(TestConstants.LDP_INDIRECT) + } + self.log("Try to create an indirect referencing a read-only resource") + r = self.do_post(writeable_location, headers=headers, body=indirect_body, admin=False) + self.assertEqual(403, r.status_code, "Did not get expected status code") + + headers = { + 'Slug': 'mockTarget', + } + self.log("Try to create a new mock target") + r = self.do_post(writeable_location, headers=headers, admin=False) + self.assertEqual(201, r.status_code, 'Did not get expected status code') + mockTarget_location = self.get_location(r) + + headers = { + "Slug": "indirect", + "Content-type": TestConstants.TURTLE_MIMETYPE, + 'Link': self.make_type(TestConstants.LDP_INDIRECT) + } + mock_body = indirect_template.format(mockTarget_location) + self.log("Create an indirect container referencing an allowed target") + r = self.do_post(writeable_location, headers=headers, body=mock_body, admin=False) + self.assertEqual(201, r.status_code, 'Did not get expected status code') + indirect_location = self.get_location(r) + + insert_delete_patch = "prefix ldp: \n" \ + "DELETE {{ <> ldp:membershipResource ?o }} \n" \ + "INSERT {{ <> ldp:membershipResource <{0}> }} \n" \ + "WHERE {{ <> ldp:membershipResource ?o }}".format(read_only_location) + self.log("Try to patch the indirect from allowed to not-allowed") + r = self.do_patch(indirect_location, headers=patch_headers, body=insert_delete_patch, admin=False) + self.assertEqual(403, r.status_code, 'Did not get expected status code') + + delete_data_body = "prefix ldp: \n" \ + "DELETE DATA {{ <> ldp:membershipResource <{0}> }}".format(mockTarget_location) + self.log("Try to DELETE DATA the membershipResource") + r = self.do_patch(indirect_location, headers=patch_headers, body=delete_data_body, admin=False) + self.assertEqual(204, r.status_code, 'Did not get expected status code') + + insert_data_body = "prefix ldp: \n" \ + "INSERT DATA {{ <> ldp:membershipResource <{0}> }}".format(read_only_location) + self.log("Try to INSERT DATA the membershipResource") + r = self.do_patch(indirect_location, headers=patch_headers, body=insert_data_body, admin=False) + self.assertEqual(403, r.status_code, 'Did not get expected status code') + + self.log("Try to patch the indirect from allowed to not-allowed as Admin") + r = self.do_patch(indirect_location, headers=patch_headers, body=insert_delete_patch) + self.assertEqual(204, r.status_code, 'Did not get expected status code') + + self.log("Now try to post to the indirect") + post_body = "@prefix ldp: .\n" \ + "@prefix test: .\n\n" \ + "<> test:something <{0}> .".format(mockTarget_location) + r = self.do_post(indirect_location, headers=turtle_headers, body=post_body, admin=False) + self.assertEqual(403, r.status_code, "Did not get expected status code") + + self.log("Indirect is {0}\nread-only is {1}".format(indirect_location, read_only_location)) diff --git a/rdf_tests.py b/rdf_tests.py index 196fdcb..74abbd0 100644 --- a/rdf_tests.py +++ b/rdf_tests.py @@ -1,4 +1,7 @@ #!/bin/env python +import time + +import rdflib.parser import TestConstants from abstract_fedora_tests import FedoraTests, register_tests, Test @@ -58,3 +61,56 @@ def testRdfSerialization(self): self.log("Test for tombstone") r = self.do_get(location) self.assertEqual(410, r.status_code, "Object's tombstone not found.") + + @Test + def TestRoundtrippingBinary(self): + self.log("Post new binary") + r = self.do_post(headers={'Content-type': 'text/plain'}, body="Content") + self.assertEqual(TestConstants.CREATED, r.status_code, "Did not create binary") + location = self.get_location(r) + self.log("location is {}".format(location)) + + body = self.do_get(location + "/" + TestConstants.FCR_METADATA, headers={'Accept': 'application/n-triples'}) + graph = rdflib.Graph() + graph.parse(data=body.content.decode(encoding='utf-8'), format='nt') + for (s, p, o) in graph: + if p in [rdflib.URIRef(TestConstants.FEDORA_NS + "hasFixityService"), + rdflib.URIRef(TestConstants.FEDORA_NS + "created"), + rdflib.URIRef(TestConstants.FEDORA_NS + "createdBy"), + rdflib.URIRef(TestConstants.FEDORA_NS + "lastModified"), + rdflib.URIRef(TestConstants.FEDORA_NS + "lastModifiedBy"), + rdflib.URIRef(TestConstants.RDF_TYPE), + rdflib.URIRef("http://www.loc.gov/premis/rdf/v1#hasMessageDigest")]: + graph.remove((s, p, o)) + + new_body = graph.serialize(format='nt') + self.log("PUTTING body {}".format(new_body)) + + self.log("Delete binary") + r = self.do_delete(location) + self.assertEqual(TestConstants.NO_CONTENT, r.status_code, "Unable to delete binary") + self.log("Delete binary tombstone") + r = self.do_delete(location + "/" + TestConstants.FCR_TOMBSTONE) + self.assertEqual(TestConstants.NO_CONTENT, r.status_code, "Unable to delete binary tombstone") + + time.sleep(1) + self.log("Put binary back") + r = self.do_put(location, headers={'Content-type': 'text/plain'}, body="Content") + self.assertEqual(TestConstants.CREATED, r.status_code, "Did not create binary") + self.log("Put the description back") + r = self.do_put(location + "/" + TestConstants.FCR_METADATA, headers={ + 'Content-type': 'application/n-triples', + 'Prefer': 'handling=lenient' + }, body=new_body) + self.assertEqual(TestConstants.NO_CONTENT, r.status_code, "Did not update binary description") + + self.log("Get the body again") + r = self.do_get(location + "/" + TestConstants.FCR_METADATA, headers={'Accept': 'application/n-triples'}) + self.assertEqual(TestConstants.OK, r.status_code, "Could not get the binary description") + get_body = r.content.decode(encoding='utf-8') + self.log("GET body {}".format(get_body)) + graph2 = rdflib.Graph() + graph2.parse(data=get_body, format='nt') + for triple in graph: + if triple not in graph2: + self.fail("Could not find triple {} in graph2".format(triple)) diff --git a/requirements.txt b/requirements.txt index fcf1077..3275541 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ PyYAML>=5.4 requests>=2.20.0 pyjq>=2.3.0 +rdflib>=5.0.0 \ No newline at end of file diff --git a/sparql_tests.py b/sparql_tests.py index 0105c67..49567e7 100644 --- a/sparql_tests.py +++ b/sparql_tests.py @@ -1,8 +1,10 @@ #!/bin/env python -import TestConstants +import TestConstants as TC from abstract_fedora_tests import FedoraTests, register_tests, Test import os +import json +import pyjq @register_tests @@ -23,13 +25,13 @@ def doSparqlContainerTest(self): headers = { 'Content-type': 'text/turtle' } - r = self.do_post(self.getBaseUri(), headers=headers, body=TestConstants.OBJECT_TTL) + r = self.do_post(self.getBaseUri(), headers=headers, body=TC.OBJECT_TTL) self.assertEqual(201, r.status_code, "Did not create container") location = self.get_location(r) self.log("Set dc:title with SPARQL") patch_headers = { - 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=patch_headers, body=self.TITLE_SPARQL) self.assertEqual(204, r.status_code, "Did not get expected result") @@ -54,7 +56,7 @@ def doSparqlBinaryTest(self): self.log("Set dc:title with SPARQL") patch_headers = { - 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(description, headers=patch_headers, body=self.TITLE_SPARQL) self.assertEqual(204, r.status_code, "Did not get expected result") @@ -78,7 +80,7 @@ def doUnicodeSparql(self): self.log("Patching with unicode") headers = { - 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=headers, body=sparql) self.assertEqual(204, r.status_code, "Did not get expected response code") @@ -97,7 +99,7 @@ def doAddType(self): "INSERT DATA { <> rdf:type example:type }" self.log("Patching with our own type") headers = { - 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=headers, body=sparql) self.assertEqual(204, r.status_code, "Did not get expected response code") @@ -113,10 +115,109 @@ def doAddRestrictedType(self): sparql = "PREFIX rdf: " \ "PREFIX ldp: " \ "PREFIX example: " \ - "INSERT DATA {{ <> rdf:type <{0}> }}".format(TestConstants.LDP_DIRECT) + "INSERT DATA {{ <> rdf:type <{0}> }}".format(TC.LDP_DIRECT) self.log("Patching with our own type") headers = { - 'Content-type': TestConstants.SPARQL_UPDATE_MIMETYPE + 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=headers, body=sparql) - self.assertwEqual(409, r.status_code, "Did not get expected response code") + self.assertEqual(409, r.status_code, "Did not get expected response code") + + @Test + def doInboundReferenceContainer(self): + reference = "http://awoods.com/pointer" + self.log("Create a container") + r = self.do_post(self.getBaseUri()) + self.checkResponse(201, r) + location = self.get_location(r) + self.log("Create a RDF container with a reference to the first container.") + rdf = "<> <{}> <{}> .".format(reference, location) + headers = { + 'Content-type': TC.TURTLE_MIMETYPE, + } + r = self.do_post(self.getBaseUri(), headers=headers, body=rdf) + self.checkResponse(201, r) + second_location = self.get_location(r) + self.log("Get first container") + r = self.do_get(location) + self.checkResponse(200, r) + self.log("Get second container") + r = self.do_get(second_location) + self.checkResponse(200, r) + self.log("Get first container with Inbound References") + headers = { + 'Prefer': "return=representation; include=\"{}\"".format(TC.INBOUND_REFERENCE), + 'Accept': TC.JSONLD_MIMETYPE + } + r = self.do_get(location, headers=headers) + self.checkResponse(200, r) + body = r.content.decode('UTF-8') + json_body = json.loads(body) + result = pyjq.all('.[] | select(."@id" == "{}") | ."{}" | .[0]."@id"'.format(second_location, reference), json_body) + self.assertEqual(location, result[0]) + + @Test + def doInboundReferenceBinary(self): + reference = "http://awoods.com/pointer" + self.log("Create a binary") + headers = { + 'Content-type': 'text/plain' + } + r = self.do_post(self.getBaseUri(), headers=headers, body="Some test text") + self.checkResponse(201, r) + location = self.get_location(r) + location_metadata = location + "/" + TC.FCR_METADATA + self.log("Create a RDF container with a reference to the binary.") + rdf = "<> <{}> <{}> .".format(reference, location) + headers = { + 'Content-type': TC.TURTLE_MIMETYPE, + } + r = self.do_post(self.getBaseUri(), headers=headers, body=rdf) + self.checkResponse(201, r) + second_location = self.get_location(r) + self.log("Get binary") + r = self.do_get(location) + self.checkResponse(200, r) + self.log("Get binary description") + r = self.do_get(location_metadata) + self.checkResponse(200, r) + self.log("Get first container") + r = self.do_get(second_location) + self.checkResponse(200, r) + self.log("Get binary description with Inbound References") + headers = { + 'Prefer': "return=representation; include=\"{}\"".format(TC.INBOUND_REFERENCE), + 'Accept': TC.JSONLD_MIMETYPE + } + r = self.do_get(location_metadata, headers=headers) + self.checkResponse(200, r) + body = r.content.decode('UTF-8') + json_body = json.loads(body) + result = pyjq.all('.[] | select(."@id" == "{}") | ."{}" | .[0]."@id"'.format(second_location, reference), + json_body) + self.assertEqual(location, result[0]) + + @Test + def testInboundReferenceToSelf(self): + reference = "http://awoods.com/pointsTo" + r = self.do_post(self.getBaseUri()) + self.checkResponse(TC.CREATED, r) + location = self.get_location(r) + + body = "INSERT {{ <> <{}> <{}> }} WHERE {{}}".format(reference, location) + headers = { + "Content-type": TC.SPARQL_UPDATE_MIMETYPE + } + r = self.do_patch(location, headers=headers, body=body) + self.checkResponse(TC.NO_CONTENT, r) + + headers = { + 'Prefer': "return=representation; include=\"{}\"".format(TC.INBOUND_REFERENCE), + 'Accept': TC.JSONLD_MIMETYPE + } + r = self.do_get(location, headers=headers) + self.checkResponse(200, r) + body = r.content.decode('UTF-8') + json_body = json.loads(body) + result = pyjq.all('.[] | select(."@id" == "{}") | ."{}" '.format(location, reference), json_body) + self.assertEqual(1, len(result)) diff --git a/ssearch_tests.py b/ssearch_tests.py new file mode 100644 index 0000000..52ff658 --- /dev/null +++ b/ssearch_tests.py @@ -0,0 +1,71 @@ +#!/bin/env python +import TestConstants as TC +from abstract_fedora_tests import FedoraTests, register_tests, Test +from FedoraSearchTool import FedoraSearchTool +import pyjq +import json + + +@register_tests +class FedoraSimpleSearchTests(FedoraTests): + + # Create test objects all inside here for easy of review + CONTAINER = "/test_search" + + search = None + + def createSomeResources(self, base=None, count=10): + """ + Create some resources and track the URIs + :param base: The base uri to POST to. + :param count: The number of resources to create. + :return: List of created resource URIs. + """ + resource_ids = list() + if base is None: + base = self.getBaseUri() + for x in range(0, count): + r = self.do_post(base) + location = self.get_location(r) + resource_ids.append(location) + return resource_ids + + def getSearch(self): + """ + Get a FedoraSearchTool instance. + :return: The FedoraSearchTool. + """ + if self.search is None: + self.search = FedoraSearchTool.create(self.getFedoraBase(), self) + return self.search + + @Test + def testSearchAll(self): + max_results = 20 + offset = 0 + items = self.createSomeResources(count=5) + found = [] + search = self.getSearch() + search.add_condition("fedora_id", "=", "*") + while len(found) < len(items) and offset < 5: + search.set_max_results(max_results) + search.set_offset((offset * max_results)) + r = search.do_query() + self.checkResponse(200, r) + body = r.content.decode('UTF-8') + body_json = json.loads(body) + for item in items: + if pyjq.first('."items" | reduce .[] as $item (false; if $item."fedora_id" == "{}"' + ' then true else . end)'.format(item), body_json): + # Found the item so add it to the list. + found.append(item) + offset += 1 + if len(found) < len(items): + self.fail("Did not find all created ids.") + + @Test + def testWithBadParameter(self): + search = self.getSearch() + search.add_condition("myField", "=", "*") + r = search.do_query() + self.checkResponse(TC.BAD_REQUEST, r) diff --git a/testrunner.py b/testrunner.py index d0b8e98..9d027df 100755 --- a/testrunner.py +++ b/testrunner.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import TestConstants @@ -19,6 +19,7 @@ from authz_tests import FedoraAuthzTests from indirect_tests import FedoraIndirectTests from camel_tests import FedoraCamelTests +from ssearch_tests import FedoraSimpleSearchTests class FedoraTestRunner: @@ -103,6 +104,9 @@ def run_tests(self): if test == 'all' or test == 'indirect': indirect = FedoraIndirectTests(self.config) indirect.run_tests() + if test == 'all' or test == 'search': + search = FedoraSimpleSearchTests(self.config) + search.run_tests() if test == 'camel': camel = FedoraCamelTests(self.config) camel.run_tests() @@ -123,7 +127,8 @@ def csv_list(string): class CSVAction(argparse.Action): - valid_options = ["authz", "basic", "sparql", "rdf", "version", "transaction", "fixity", "indirect", "camel"] + valid_options = ["authz", "basic", "sparql", "rdf", "version", "transaction", "fixity", "indirect", "camel", + "search"] def __call__(self, parser, args, values, option_string=None): if isinstance(values, list): diff --git a/transaction_tests.py b/transaction_tests.py index 80ef229..86d1cfe 100644 --- a/transaction_tests.py +++ b/transaction_tests.py @@ -1,6 +1,6 @@ #!/bin/env python -import TestConstants +import TestConstants as TC from abstract_fedora_tests import FedoraTests, register_tests, Test @@ -10,16 +10,22 @@ class FedoraTransactionTests(FedoraTests): # Create test objects all inside here for easy of review CONTAINER = "/test_transaction" - def get_transaction_provider(self): - headers = { - 'Accept': TestConstants.JSONLD_MIMETYPE - } - r = self.do_head(self.getFedoraBase(), headers=headers) - self.assertEqual(200, r.status_code, "Did not get expected response") - link_headers = self.get_link_headers(r) - if TestConstants.FEDORA_TX_ENDPOINT_REL in link_headers.keys(): - return link_headers.get(TestConstants.FEDORA_TX_ENDPOINT_REL)[0] - return None + def createTransaction(self, admin=None): + if admin is None: + admin = True + tx_location = self.get_transaction_provider() + self.log("Create a transaction") + r = self.do_post(tx_location, admin=admin) + self.checkResponse(201, r) + return self.get_location(r) + + def checkResponse(self, expected, response, tx_id = None): + try: + super().checkResponse(expected, response) + except AssertionError as e: + if tx_id is not None: + self.do_delete(tx_id) + raise e @Test def doCommitTest(self): @@ -28,15 +34,12 @@ def doCommitTest(self): self.log("Could not location transaction provider") self.log("Skipping test") else: - self.log("Create a transaction") - r = self.do_post(tx_provider) - self.assertEqual(201, r.status_code, "Did not get expected response code") - transaction_id = self.get_location(r) + transaction_id = self.createTransaction() self.log("Transaction is {0}".format(transaction_id)) self.log("Get status of transaction") r = self.do_get(transaction_id) - self.assertEqual(204, r.status_code, "Did not get expected response code") + self.checkResponse(TC.NO_CONTENT, r, transaction_id) self.assertHeaderExists(r, "Atomic-Expires") self.log("Create an container in the transaction") @@ -44,59 +47,59 @@ def doCommitTest(self): 'Atomic-Id': transaction_id } r = self.do_post(headers=transaction_headers) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(TC.CREATED, r, transaction_id) transaction_obj = self.get_location(r) self.log("Container is available inside the transaction") r = self.do_get(transaction_obj, headers=transaction_headers) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(TC.OK, r, transaction_id) self.log("Container not available outside the transaction") r = self.do_get(transaction_obj) - self.assertEqual(404, r.status_code, "Did not get expected response code") + self.checkResponse(TC.NOT_FOUND, r, transaction_id) self.log("Use an invalid transaction ID") bad_headers = { 'Atomic-ID': 'this-is-a-failure' } r = self.do_post(headers=bad_headers) - self.assertEqual(409, r.status_code, "Did not get expected response code") + self.checkResponse(TC.CONFLICT, r, transaction_id) self.log("Use the bare UUID of a valid transaction ID") diff_headers = { - 'Atomic-ID': transaction_id.replace(self.getFedoraBase() + "/" + TestConstants.FCR_TX + "/", "") + 'Atomic-ID': transaction_id.replace(self.getFedoraBase() + "/" + TC.FCR_TX + "/", "") } r = self.do_post(headers=diff_headers) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(TC.CREATED, r, transaction_id) second_obj = self.get_location(r) self.log("Second container is available inside the transaction") r = self.do_get(second_obj, headers=transaction_headers) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(TC.OK, r, transaction_id) self.log("Second container not available outside the transaction") r = self.do_get(second_obj) - self.assertEqual(404, r.status_code, "Did not get expected response code") + self.checkResponse(TC.NOT_FOUND, r, transaction_id) - self.log("Try to commit with POST") - r = self.do_post(transaction_id + "/commit") - self.assertEqual(405, r.status_code, "Did not get expected response code") + self.log("Try to commit with old commit endpoint") + r = self.do_put(transaction_id + "/commit") + self.checkResponse(TC.NOT_FOUND, r, transaction_id) self.log("Commit transaction") - r = self.do_put(transaction_id + "/commit") - self.assertEqual(204, r.status_code, "Did not get expected response code") + r = self.do_put(transaction_id) + self.checkResponse(TC.NO_CONTENT, r, transaction_id) self.log("Container is now available outside the transaction") r = self.do_get(transaction_obj) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(TC.OK, r) self.log("Transaction is no longer available") r = self.do_get(transaction_id) - self.assertEqual(410, r.status_code, "Did not get expected response code") + self.checkResponse(TC.GONE, r) self.log("Can't use the transaction anymore") r = self.do_post(headers=transaction_headers) - self.assertEqual(409, r.status_code, "Did not get expected response code") + self.checkResponse(TC.CONFLICT, r) @Test def doRollbackTest(self): @@ -105,10 +108,7 @@ def doRollbackTest(self): self.log("Could not location transaction provider") self.log("Skipping test") else: - self.log("Create a transaction") - r = self.do_post(tx_provider) - self.assertEqual(201, r.status_code, "Did not get expected response code") - transaction_id = self.get_location(r) + transaction_id = self.createTransaction() self.log("Transaction is {0}".format(transaction_id)) self.log("Create an container in the transaction") @@ -116,29 +116,195 @@ def doRollbackTest(self): 'Atomic-Id': transaction_id } r = self.do_post(headers=transaction_headers) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(TC.CREATED, r, transaction_id) transaction_obj = self.get_location(r) self.log("Container is available inside the transaction") r = self.do_get(transaction_obj, headers=transaction_headers) - self.assertEqual(200, r.status_code, "Did not get expected response code") + self.checkResponse(TC.OK, r, transaction_id) self.log("Container not available outside the transaction") r = self.do_get(transaction_obj) - self.assertEqual(404, r.status_code, "Did not get expected response code") + self.checkResponse(TC.NOT_FOUND, r, transaction_id) self.log("Rollback transaction") r = self.do_delete(transaction_id) - self.assertEqual(204, r.status_code, "Did not get expected response code") + self.checkResponse(TC.NO_CONTENT, r, transaction_id) self.log("Container is still not available outside the transaction") r = self.do_get(transaction_obj) - self.assertEqual(404, r.status_code, "Did not get expected response code") + self.checkResponse(TC.NOT_FOUND, r) self.log("Transaction is no longer available") r = self.do_get(transaction_id) - self.assertEqual(410, r.status_code, "Did not get expected response code") + self.checkResponse(TC.GONE, r) self.log("Can't use the transaction anymore") r = self.do_post(headers=transaction_headers) - self.assertEqual(409, r.status_code, "Did not get expected response code") + self.checkResponse(TC.CONFLICT, r) + + @Test + def createAndDeleteInTwoTransaction(self): + + self.log("Create a transaction") + tx_id = self.createTransaction() + + self.log("Create a container") + headers = { + TC.ATOMIC_ID_HEADER: tx_id + } + r = self.do_post(headers=headers) + self.checkResponse(TC.CREATED, r, tx_id) + container_location = self.get_location(r) + + self.log("Get the container") + r = self.do_get(container_location, headers=headers) + self.checkResponse(TC.OK, r, tx_id) + + self.log("Commit the transaction") + r = self.do_put(tx_id) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log("Get the container outside transaction") + r = self.do_get(container_location) + self.checkResponse(TC.OK, r) + + self.log("Create a new transaction") + tx_id = self.createTransaction() + headers = { + TC.ATOMIC_ID_HEADER: tx_id + } + + self.log("Delete the container") + r = self.do_delete(container_location, headers=headers) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log("Container exists outside the transaction") + r = self.do_get(container_location) + self.checkResponse(TC.OK, r, tx_id) + + self.log("Container does not exist inside the transaction") + r = self.do_get(container_location, headers=headers) + self.checkResponse(TC.GONE, r, tx_id) + + self.log("Commit the transaction") + r = self.do_put(tx_id) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log("Container does not exist outside the transaction") + r = self.do_get(container_location) + self.checkResponse(TC.GONE, r, tx_id) + + @Test + def createAndDeleteInOneTransaction(self): + + tx_id = self.createTransaction() + + self.log("Create a container") + headers = { + TC.ATOMIC_ID_HEADER: tx_id + } + r = self.do_post(headers=headers) + self.checkResponse(TC.CREATED, r, tx_id) + container_location = self.get_location(r) + + self.log("Get the container: {}".format(container_location)) + r = self.do_get(container_location, headers=headers) + self.checkResponse(TC.OK, r, tx_id) + + self.log("Delete the container") + r = self.do_delete(container_location, headers=headers) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log("Check its NOT FOUND") + r = self.do_get(container_location, headers=headers) + self.checkResponse(TC.NOT_FOUND, r, tx_id) + + self.log("Commit the transaction") + r = self.do_put(tx_id) + self.checkResponse(TC.NO_CONTENT, r) + + @Test + def testTransactionExclusion(self): + self.log("Create a container.") + r = self.do_post() + self.checkResponse(TC.CREATED, r) + container_id = self.get_location(r) + + self.log("Get the container") + r = self.do_get(container_id) + self.checkResponse(TC.OK, r) + + tx_location = self.createTransaction() + + self.log("Delete the container in the transaction") + txheaders = { + TC.ATOMIC_ID_HEADER: tx_location + } + r = self.do_delete(container_id, headers=txheaders) + self.checkResponse(TC.NO_CONTENT, r, tx_location) + self.log("Ensure the container is removed in the transaction.") + r = self.do_get(container_id, headers=txheaders) + self.checkResponse(TC.GONE, r, tx_location) + self.log("Inside a transaction delete the tombstone.") + r = self.do_delete(container_id + "/" + TC.FCR_TOMBSTONE, headers=txheaders) + self.checkResponse(TC.NO_CONTENT, r, tx_location) + self.log("Ensure the container is totally removed in the transaction.") + r = self.do_get(container_id, headers=txheaders) + self.checkResponse(TC.NOT_FOUND, r, tx_location) + self.log("Extend the transaction") + r = self.do_post(tx_location) + self.checkResponse(TC.NO_CONTENT, r, tx_location) + r = self.do_post(tx_location) + self.checkResponse(TC.NO_CONTENT, r, tx_location) + + self.log("Inside the transaction put back the container.") + r = self.do_put(container_id, headers=txheaders) + self.checkResponse(TC.CREATED, r, tx_location) + self.log("Commit the transaction.") + r = self.do_put(tx_location) + self.checkResponse(TC.NO_CONTENT, r, tx_location) + self.log("Verify you can still get the container.") + r = self.do_get(container_id) + self.checkResponse(TC.OK, r) + + ''' Waiting on https://fedora-repository.atlassian.net/browse/FCREPO-3827 ''' + #@Test + def aPlainUserTransactionRollback(self): + self.log("Create a resource") + r = self.do_post() + self.checkResponse(TC.CREATED, r) + container_id = self.get_location(r) + + auth = "@prefix acl: <{0}>.\n" \ + "@prefix fedora: <{1}>.\n" \ + "<#container> a acl:Authorization ;\n" \ + " acl:mode acl:Read, acl:Write, acl:Append, acl:Control ;\n" \ + " acl:accessTo <{2}> ;\n" \ + " acl:default <{2}> ;\n" \ + " acl:agent \"{3}\" .\n".format(TC.ACL_NS, TC.FEDORA_NS, container_id, self.config[TC.USER_NAME_PARAM]) + + self.log("Set up ACL with user having full access.") + r = self.do_put(container_id + "/" + TC.FCR_ACL, headers={"Content-type": "text/turtle"}, body=auth, admin=True) + self.checkResponse(TC.CREATED, r) + + self.log("Have user read the item") + r = self.do_get(container_id, admin=False) + self.checkResponse(TC.OK, r) + self.log("Have the user patch the item") + patch = "INSERT DATA { <> \"Some title\" }" + r = self.do_patch(container_id, headers={"Content-type": "application/sparql-update"}, body=patch, admin=False) + self.checkResponse(TC.NO_CONTENT, r) + self.log("Start a transaction") + tx_id = self.createTransaction(False) + tx_headers = {TC.ATOMIC_ID_HEADER: tx_id} + self.log("Add a child object") + r = self.do_post(container_id, headers=tx_headers, admin=False) + self.checkResponse(TC.CREATED, r, tx_id) + child = self.get_location(r) + self.log("Test getting the child (" + child + ") in transaction") + r = self.do_get(child, admin=False, headers=tx_headers) + self.checkResponse(TC.OK, r, tx_id) + self.log("Rollback transaction") + r = self.do_delete(tx_id, admin=False) + self.checkResponse(TC.NO_CONTENT, r, tx_id) diff --git a/version_tests.py b/version_tests.py index 25f78d9..05e4e4c 100644 --- a/version_tests.py +++ b/version_tests.py @@ -3,6 +3,14 @@ import TestConstants from abstract_fedora_tests import FedoraTests, register_tests, Test import time +import rdflib +from rdflib.namespace import DC, RDF + + +def getVersionEndpoint(uri): + if not uri.endswith("/" + TestConstants.FCR_VERSIONS): + uri += "/" + TestConstants.FCR_VERSIONS + return uri @register_tests @@ -12,16 +20,19 @@ class FedoraVersionTests(FedoraTests): CONTAINER = "/test_version" @staticmethod - def count_mementos(response): + def get_all_mementos(response): body = response.content.decode('UTF-8') mementos = [x for x in body.split('\n') if x.find("rel=\"memento\"") >= 0] - return len(mementos) + return mementos + + @staticmethod + def count_mementos(response): + return len(FedoraVersionTests.get_all_mementos(response)) def checkMementoCount(self, expected, uri, admin=None): if admin is None: admin = True - if not uri.endswith("/" + TestConstants.FCR_VERSIONS): - uri += "/" + TestConstants.FCR_VERSIONS + uri = getVersionEndpoint(uri) headers = { 'Accept': TestConstants.LINK_FORMAT_MIMETYPE } @@ -29,23 +40,43 @@ def checkMementoCount(self, expected, uri, admin=None): self.checkResponse(200, r) self.checkValue(expected, self.count_mementos(r)) - @Test + def getNthMemento(self, uri, memento_number=1, admin=None): + if admin is None: + admin = True + uri = getVersionEndpoint(uri) + headers = { + 'Accept': TestConstants.LINK_FORMAT_MIMETYPE + } + r = self.do_get(uri, headers=headers, admin=admin) + self.checkResponse(TestConstants.OK, r) + mementos = self.get_all_mementos(r) + # Remove one to match array numbering + memento_number -= 1 + if len(mementos) > memento_number: + return mementos[memento_number] + else: + self.fail("Count not get the {} memento, only found {}".format(memento_number+1, len(mementos))) + + # Waiting on https://fedora-repository.atlassian.net/browse/FCREPO-3655 + # @Test def doContainerVersioningTest(self): headers = { 'Link': self.make_type(TestConstants.LDP_BASIC) } r = self.do_post(self.getBaseUri(), headers=headers) self.log("Create a basic container") - self.checkResponse(201, r) + self.checkResponse(TestConstants.CREATED, r) location = self.get_location(r) + self.log("at " + location) version_endpoint = location + "/" + TestConstants.FCR_VERSIONS r = self.do_get(version_endpoint) - self.checkResponse(200, r) + self.checkResponse(TestConstants.OK, r) self.log("Create a version") r = self.do_post(version_endpoint) - self.checkResponse(201, r) + self.checkResponse(TestConstants.CREATED, r) + memento_location = self.get_location(r) self.log("Get the resource content") headers = { @@ -62,25 +93,13 @@ def doContainerVersioningTest(self): 'Prefer': TestConstants.PUT_PREFER_LENIENT, 'Memento-Datetime': new_date } - self.log("Check you must provide a valid body") - r = self.do_post(version_endpoint, headers=headers, body="[]") - self.checkResponse(400, r) - - self.log("Create a version with provided datetime") - r = self.do_post(version_endpoint, headers=headers, body=body) - self.checkResponse(201, r) - memento_location = self.get_location(r) + self.log("Try to create a version with provided datetime (Fedora 6)") + r = self.do_post(version_endpoint, headers=headers) + self.checkResponse(TestConstants.BAD_REQUEST, r) - self.log("Try creating another version at the same location") + self.log("Try to create a version with provided datetime and body (Fedora 6)") r = self.do_post(version_endpoint, headers=headers, body=body) - self.checkResponse(409, r) - - self.log("Check memento exists") - r = self.do_get(memento_location) - self.checkResponse(200, r) - found_datetime = r.headers['Memento-Datetime'] - self.assertIsNotNone(found_datetime, "Did not find Memento-Datetime header") - self.assertEqual(0, self.compare_rfc_dates(new_date, found_datetime), "Returned Memento-Datetime did not match sent") + self.checkResponse(TestConstants.BAD_REQUEST, r) self.log("Patch the original resource") sparql_body = "prefix dc: " \ @@ -91,45 +110,29 @@ def doContainerVersioningTest(self): "Content-Type": TestConstants.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=headers, body=sparql_body) - self.checkResponse(204, r) + self.checkResponse(TestConstants.NO_CONTENT, r) self.log("Try to patch the memento") r = self.do_patch(memento_location, headers=headers, body=sparql_body) - self.checkResponse(405, r) + self.checkResponse(TestConstants.METHOD_NOT_ALLOWED, r) self.log("Wait a second to change the time.") time.sleep(1) self.log("Create another version") r = self.do_post(version_endpoint) - self.checkResponse(201, r) + self.checkResponse(TestConstants.CREATED, r) self.log("Count mementos") - self.checkMementoCount(3, version_endpoint) - - self.log("Delete a Memento") - r = self.do_delete(memento_location) - self.checkResponse(204, r) - - self.log("Validate delete") - r = self.do_get(memento_location) - self.checkResponse(404, r) - - self.log("Validate delete with another count") self.checkMementoCount(2, version_endpoint) - self.log("Create a memento at the deleted datetime") - headers = { - 'Content-Type': TestConstants.JSONLD_MIMETYPE, - 'Prefer': TestConstants.PUT_PREFER_LENIENT, - 'Memento-Datetime': new_date - } - r = self.do_post(version_endpoint, headers=headers, body=body) - self.checkResponse(201, r) + self.log("Try to delete a Memento") + r = self.do_delete(memento_location) + self.checkResponse(TestConstants.METHOD_NOT_ALLOWED, r) - self.log("Check the memento exists again") + self.log("Check memento still exists.") r = self.do_get(memento_location) - self.checkResponse(200, r) + self.checkResponse(TestConstants.OK, r) @Test def doBinaryVersioningTest(self): @@ -142,56 +145,51 @@ def doBinaryVersioningTest(self): r = self.do_post(self.getBaseUri(), headers=headers, files=files) self.log("Create a NonRdfSource") - self.checkResponse(201, r) + self.checkResponse(TestConstants.CREATED, r) location = self.get_location(r) + self.log("URI is {}".format(location)) description_location = self.find_binary_description(r) version_endpoint = location + "/" + TestConstants.FCR_VERSIONS description_version_endpoint = description_location + "/" + TestConstants.FCR_VERSIONS + self.checkMementoCount(1, location) + self.checkMementoCount(1, description_location) + self.log("Get version endpoint") r = self.do_get(version_endpoint) - self.checkResponse(200, r) + self.checkResponse(TestConstants.OK, r) self.log("Get description version endpoint") r = self.do_get(description_version_endpoint) - self.checkResponse(200, r) + self.checkResponse(TestConstants.OK, r) self.log("Create a version") r = self.do_post(version_endpoint) - self.checkResponse(201, r) + self.checkResponse(TestConstants.CREATED, r) self.log("Try to create another version too quickly") r = self.do_post(version_endpoint) - self.checkResponse(409, r) + self.checkResponse(TestConstants.CREATED, r) self.assertNotEqual(version_endpoint, description_version_endpoint) self.log("Try to create a version of the description quickly") r = self.do_post(description_version_endpoint) - self.checkResponse(409, r) + self.checkResponse(TestConstants.CREATED, r) + + self.checkMementoCount(4, location) + self.checkMementoCount(4, description_location) new_date = FedoraTests.get_rfc_date("2000-06-01 08:21:00") - self.log("Create a version with provided datetime") + self.log("Try to create a version with provided datetime") headers = { 'Content-Type': 'text/csv', 'Memento-Datetime': new_date } r = self.do_post(version_endpoint, headers=headers, files=files) - self.checkResponse(201, r) - memento_location = self.get_location(r) - - self.log("Try creating another version at the same location") - r = self.do_post(version_endpoint, headers=headers, files=files) - self.checkResponse(409, r) - - self.log("Check memento exists") - r = self.do_head(memento_location) - self.checkResponse(200, r) - found_datetime = r.headers['Memento-Datetime'] - self.assertIsNotNone(found_datetime, "Did not find Memento-Datetime header") - self.assertEqual(0, self.compare_rfc_dates(new_date, found_datetime), "Returned Memento-Datetime did not match sent") + self.checkResponse(TestConstants.BAD_REQUEST, r) self.log("PUT to the original resource") files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\nevent,more,data,tosend\n')} @@ -199,48 +197,41 @@ def doBinaryVersioningTest(self): 'Content-Type': 'text/csv' } r = self.do_put(location, headers=headers, files=files) - self.checkResponse(204, r) + self.checkResponse(TestConstants.NO_CONTENT, r) + + self.log("Create a memento with a simple POST") + r = self.do_post(version_endpoint) + self.checkResponse(TestConstants.CREATED, r) + memento_location = self.get_location(r) + + self.log("Try to GET the memento") + r = self.do_get(memento_location) + self.checkResponse(TestConstants.OK, r) self.log("Try to put to the memento") r = self.do_put(memento_location, headers=headers, files=files) - self.checkResponse(405, r) + self.checkResponse(TestConstants.METHOD_NOT_ALLOWED, r) self.log("Wait one second to change the time.") time.sleep(1) self.log("Create another version") r = self.do_post(version_endpoint) - self.checkResponse(201, r) + self.checkResponse(TestConstants.CREATED, r) self.log("Count mementos") - self.checkMementoCount(3, version_endpoint) + self.checkMementoCount(7, version_endpoint) - self.log("Delete a Memento") + self.log("Try to DELETE the memento") r = self.do_delete(memento_location) - self.checkResponse(204, r) - - self.log("Validate delete") - r = self.do_get(memento_location) - self.checkResponse(404, r) - - self.log("Validate delete with another count") - self.checkMementoCount(2, version_endpoint) - - self.log("Create a memento at the deleted datetime") - headers = { - 'Content-Type': TestConstants.JSONLD_MIMETYPE, - 'Prefer': TestConstants.PUT_PREFER_LENIENT, - 'Memento-Datetime': new_date - } - r = self.do_post(version_endpoint, headers=headers, files=files) - self.checkResponse(201, r) + self.checkResponse(TestConstants.METHOD_NOT_ALLOWED, r) self.log("Check the memento exists again") r = self.do_head(memento_location) - self.checkResponse(200, r) + self.checkResponse(TestConstants.OK, r) self.log("Validate count of mementos again") - self.checkMementoCount(3, version_endpoint) + self.checkMementoCount(7, version_endpoint) @Test def checkBinaryVersioning(self): @@ -266,22 +257,22 @@ def checkBinaryVersioning(self): self.log("Count Mementos of binary") r = self.do_get(binary_versions, headers=link_headers) self.checkResponse(200, r) - self.checkValue(0, self.count_mementos(r)) + self.checkValue(1, self.count_mementos(r)) self.log("Count Mementos of binary description") r = self.do_get(metadata_versions, headers=link_headers) self.checkResponse(200, r) - self.checkValue(0, self.count_mementos(r)) + self.checkValue(1, self.count_mementos(r)) self.log("Create version of binary from existing") r = self.do_post(binary_versions) self.checkResponse(201, r) self.log("Count Mementos of binary") - self.checkMementoCount(1, binary_versions) + self.checkMementoCount(2, binary_versions) self.log("Count Mementos of binary description") - self.checkMementoCount(1, metadata_versions) + self.checkMementoCount(2, metadata_versions) self.log("Wait a second") time.sleep(1) @@ -291,10 +282,10 @@ def checkBinaryVersioning(self): self.checkResponse(201, r) self.log("Count Mementos of binary") - self.checkMementoCount(1, binary_versions) + self.checkMementoCount(3, binary_versions) self.log("Count Mementos of binary description") - self.checkMementoCount(2, metadata_versions) + self.checkMementoCount(3, metadata_versions) @Test def createBinaryVersionsAtSameTime(self): @@ -317,9 +308,9 @@ def createBinaryVersionsAtSameTime(self): version_endpoint = location + "/" + TestConstants.FCR_VERSIONS description_version_endpoint = description_location + "/" + TestConstants.FCR_VERSIONS - self.log("Check we have no mementos of binary or description") - self.checkMementoCount(0, version_endpoint) - self.checkMementoCount(0, description_version_endpoint) + self.log("Check we have a single mementos of binary and description (auto-versioning)") + self.checkMementoCount(1, version_endpoint) + self.checkMementoCount(1, description_version_endpoint) the_date = FedoraTests.get_rfc_date('2019-05-21 18:30:00') files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\nevent,more,data,tosend\n')} @@ -328,14 +319,14 @@ def createBinaryVersionsAtSameTime(self): 'Memento-Datetime': the_date } - self.log("Make version for {0} of binary".format(the_date)) + self.log("Can't make version for {0} of binary with a body".format(the_date)) r = self.do_post(version_endpoint, headers=headers, files=files) - self.checkResponse(201, r) + self.checkResponse(TestConstants.BAD_REQUEST, r) - self.log("Check we only made a memento of the binary") + self.log("Check we haven't made a memento of the binary") self.checkMementoCount(1, version_endpoint) - self.log("Check we still have no description mementos") - self.checkMementoCount(0, description_version_endpoint) + self.log("Check we haven't made a memento of the description") + self.checkMementoCount(1, description_version_endpoint) headers = { 'Content-type': TestConstants.TURTLE_MIMETYPE, @@ -343,12 +334,80 @@ def createBinaryVersionsAtSameTime(self): 'Prefer': TestConstants.PUT_PREFER_LENIENT } - self.log("Make version for {0} of binary description".format(the_date)) + self.log("Can't make version for {0} of binary description with a body".format(the_date)) r = self.do_post(description_version_endpoint, headers=headers, body=new_body) - self.checkResponse(201, r) + self.checkResponse(TestConstants.BAD_REQUEST, r) self.log("Count binary mementos") self.checkMementoCount(1, version_endpoint) self.log("Count binary description mementos") self.checkMementoCount(1, description_version_endpoint) + + @Test + def testMementoAreInaccessibleAfterDelete(self): + r = self.do_post() + self.checkResponse(TestConstants.CREATED, r) + uri = self.get_location(r) + + self.verifyGet(uri) + + r = self.do_post(uri + "/" + TestConstants.FCR_VERSIONS) + self.checkResponse(TestConstants.CREATED, r) + memento = self.get_location(r) + + self.verifyGet(memento) + + r = self.do_delete(uri) + self.checkResponse(TestConstants.NO_CONTENT, r) + + self.verifyGone(uri) + self.verifyGone(uri + "/" + TestConstants.FCR_VERSIONS) + self.verifyGone(memento) + + @Test + def testBinaryDescription(self): + headers = { + 'Content-type': 'text/plain' + } + r = self.do_post(self.getBaseUri(), headers=headers, body="Some example text") + self.checkResponse(TestConstants.CREATED, r) + location = self.get_location(r) + description_uri = location + "/" + TestConstants.FCR_METADATA + test_type = "http://example.org/customType" + + headers = { + 'Content-type': "application/sparql-update" + } + update_string = "INSERT { <> <" + TestConstants.DC_TITLE + "> \"Original\" ." \ + " <> <" + TestConstants.RDF_TYPE + "> <" + test_type + "> } WHERE { }" + + r1 = self.do_patch(description_uri, update_string, headers=headers) + self.checkResponse(TestConstants.NO_CONTENT, r1) + + r2 = self.do_post(description_uri + "/" + TestConstants.FCR_VERSIONS) + self.checkResponse(TestConstants.CREATED, r2) + memento = self.get_location(r2) + + changed_string = "INSERT { <" + location + "> <" + TestConstants.DC_TITLE + "> \"Updated\". } WHERE { }" + r3 = self.do_patch(description_uri, changed_string, headers=headers) + self.checkResponse(TestConstants.NO_CONTENT, r3) + + r4 = self.do_get(memento, headers={'Accept': 'application/n-triples'}) + self.checkResponse(TestConstants.OK, r4) + graph = rdflib.Graph() + graph.parse(data=r4.content, format="nt") + + subject_uri = rdflib.URIRef(location) + + # Have to use assertTrue( triple NOT IN graph) or it throws an exception. + self.assertTrue("Property added to original before versioning must appear", + (subject_uri, DC.title, rdflib.Literal("Original")) in graph) + self.assertTrue("Property added after memento created must not appear", + (subject_uri, DC.title, rdflib.Literal("Updated")) not in graph) + self.assertTrue("Memento type should not be visible", + (subject_uri, RDF.type, rdflib.URIRef(TestConstants.MEM_MEMENTO)) not in graph) + self.assertTrue("Must have binary type", + (subject_uri, RDF.type, rdflib.URIRef(TestConstants.FEDORA_BINARY)) in graph) + self.assertTrue("Must have custom type", + (subject_uri, RDF.type, rdflib.URIRef(test_type)) in graph) From 148a0552845acf3ff6e3f1d18f92110c6407c427 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 3 Mar 2023 16:38:27 -0600 Subject: [PATCH 13/20] Fix some tests Make less verbose reporting --- TestConstants.py | 1 + abstract_fedora_tests.py | 37 ++++++++++--- authz_tests.py | 2 +- basic_interaction_tests.py | 111 +++++++++++++++++++++++++++++++------ fixity_tests.py | 2 +- testrunner.py | 95 ++++++++++++++++++------------- version_tests.py | 25 ++++++--- 7 files changed, 196 insertions(+), 77 deletions(-) diff --git a/TestConstants.py b/TestConstants.py index fd35f02..042d58e 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -92,6 +92,7 @@ GONE = 410 BAD_REQUEST = 400 METHOD_NOT_ALLOWED = 405 +SERVER_ERROR = 500 # Some standard properties DC_NAMESPACE = "http://purl.org/dc/elements/1.1/" diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index 8229cf7..71b1df3 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -8,6 +8,7 @@ import inspect import pyjq import json +import os from datetime import datetime, timezone from email.utils import format_datetime @@ -21,8 +22,13 @@ class FedoraTests(unittest.TestCase): # Holds the configuration config = {} + # Holds ultimate success or failure for tests. + results = {} + def __init__(self, config): super().__init__() + if 'debug_level' not in config: + config['debug_level'] = 1 self.config = config def getBaseUri(self): @@ -33,11 +39,16 @@ def getFedoraBase(self): """ Return the Fedora Base URI """ return self.config[TestConstants.BASE_URL_PARAM] + @staticmethod + def getImagePath(): + return os.path.join(os.getcwd(), 'resources', 'basic_image.jpg'); + @staticmethod def getCurrentClass(): return inspect.stack()[1][0].f_locals['self'].__class__.__name__ def run_tests(self): + self.results = {} # Reset results variable current = FedoraTests.getCurrentClass() self.check_for_retest(self.getBaseUri()) self.log("\nStarting class {0}\n".format(current)) @@ -46,8 +57,13 @@ def run_tests(self): for test in self._testdict: method = getattr(self, test) self.log("Running {0}".format(test)) - method() - self.log("Passed\n") + try: + method() + self.results[test] = {'result': True} + self.log("Passed\n") + except AssertionError as e: + self.results[test] = {'result': False, 'message': str(e)} + self.log("Failed\n") self.cleanup(self.getBaseUri()) self.log("\nExiting class {0}".format(current)) @@ -233,6 +249,12 @@ def check_for_retest(self, uri): "in the configuration.") quit() + def getHeader(self, uri, header_name): + r = self.do_head(uri) + self.assertTrue(TestConstants.OK, r) + if header_name in r.headers: + return r.headers[header_name] + @staticmethod def get_link_headers(response): """ Get the response's LINK headers, returned as a dict of key -> list() @@ -337,10 +359,9 @@ def assertContrainedByHeaderExists(self, response): """ Check for link headers with the constrainedby rel type """ self.assertLinkHeaderExists(response, "http://www.w3.org/ns/ldp#constrainedBy") - - @staticmethod - def log(message): - print(message) + def log(self, message): + if 'debug_level' in self.config and self.config['debug_level'] > 0: + print(message) def find_binary_description(self, response): headers = FedoraTests.get_link_headers(response) @@ -351,8 +372,8 @@ def checkResponse(self, expected, response): self.checkValue(expected, response.status_code) def checkValue(self, expected, received): - self.assertEqual(expected, received, "Expected {} but received {}".format(expected, received)) - FedoraTests.log(" Passed {0} == {0}".format(received)) + self.assertEqual(expected, received, f"Expected {expected} but received {received}") + self.log(" Passed {0} == {0}".format(received)) def createBasicContainer(self, parent_location): """ Make a simple basic container with some RDF content. """ diff --git a/authz_tests.py b/authz_tests.py index 7b31bd6..48ab954 100644 --- a/authz_tests.py +++ b/authz_tests.py @@ -450,7 +450,7 @@ def testContainerWithAccessToClass(self): self.log("Try to get memento as test user 1") r = self.do_get(memento_location, admin=False) - self.checkResponse(403, r) + self.checkResponse(200, r) self.log("Try to get memento as test user 2") r = self.do_get(memento_location, admin=user2) diff --git a/basic_interaction_tests.py b/basic_interaction_tests.py index c432aed..8cc9fb0 100644 --- a/basic_interaction_tests.py +++ b/basic_interaction_tests.py @@ -1,4 +1,7 @@ #!/bin/env python +import shutil +import tempfile +import time import TestConstants as TC from abstract_fedora_tests import FedoraTests, register_tests, Test @@ -20,18 +23,33 @@ def createTestResource(self, type, files=None): headers = { 'Link': link_type } + self.log("Create the resource") r = self.do_post(self.getBaseUri(), headers=headers, files=files) - self.assertEqual(201, r.status_code, "Did not create container") + self.checkResponse(TC.CREATED, r) location = self.get_location(r) self.nodes.append(location) + self.log("GET the resource") r = self.do_get(location) - self.assertEqual(200, r.status_code, "Did not get container") - self.assertIsNotNone(r.headers['Link'], "Did not get any link headers returned") + self.checkResponse(TC.OK, r) + self.assertHeaderExists(r, 'Link') type_headers = FedoraTests.get_link_headers(r) self.assertIsNotNone(type_headers['type'], "Did not get any link headers with rel=type") self.assertIn(type, type_headers['type'], "Did not find link header for {}".format(type)) return location + def getEtag(self, uri): + """ Get the eTag header for a specified URI. """ + return self.getHeader(uri, "ETag") + + def getStateToken(self, uri): + """ Get the X-State-Token header for a specified URI. """ + return self.getHeader(uri, "X-State-Token") + + def duplicateImage(self): + new_file = os.path.join(tempfile.gettempdir(), 'temp_image.jpeg') + shutil.copyfile(self.getImagePath(), new_file) + return new_file + @Test def aTestMissingResource(self): fake_id = str(uuid.uuid4()) @@ -79,7 +97,6 @@ def testDeleteAResource(self): r = self.do_put(container_location) self.assertEqual(201, r.status_code, "Did not get expected response") - @Test def testBasicContainer(self): self.createTestResource(TC.LDP_BASIC) @@ -319,33 +336,91 @@ def testBinaryTriples(self): r = self.do_post(self.getBaseUri(), headers=headers, body="some sample text") self.checkResponse(TC.CREATED, r) + @Test def testChecksum(self): self.log("Create parent resource") - r = self.do_post(self.getBaseUri()); + r = self.do_post(self.getBaseUri()) self.checkResponse(TC.CREATED, r) parent_uri = self.get_location(r) first_etag = self.getEtag(parent_uri) + self.log(f"First etag of parent is {first_etag}") + self.log("Add child resource to parent") r = self.do_post(parent_uri) self.checkResponse(TC.CREATED, r) second_etag = self.getEtag(parent_uri) - - self.assertNotEqual(first_etag, second_etag) - + self.log(f"Second etag of parent is {second_etag}") + + self.log("Add 30 child resources to parent") for i in range(1, 30): - r = self.do_post(parent_uri, {'Slug': 'child_' + i}) + r = self.do_post(parent_uri, {'Slug': 'child_' + str(i)}) self.checkResponse(TC.CREATED, r) third_etag = self.getEtag(parent_uri) + self.log(f"Third etag of parent is {third_etag}") + + self.assertNotEqual(first_etag, second_etag, "First and second state etags should not match") + self.assertNotEqual(first_etag, third_etag, "First and third state etag should not match") + self.assertNotEqual(second_etag, third_etag, "Second and third state etag should not match") + + # @Test # These tests require Fedora to allow the paths, might require more thought. + def testExternalContentProxyLocal(self): + self.log("Create external content to local resource") + image_path = self.duplicateImage() + external_headers = { + "Link": "; rel =\"http://fedora.info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/jpeg\"".format(image_path) + } + r = self.do_post(headers=external_headers) + self.checkResponse(TC.CREATED, r) + location = self.get_location(r) + r = self.do_get(location) + self.checkResponse(TC.OK, r) + self.assertHeaderExists(r, "Content-type", "image/jpeg") + self.log("Delete the local file") + os.unlink(image_path) + r = self.do_get(location) + self.checkResponse(TC.SERVER_ERROR, r) + + # @Test # These tests require Fedora to allow the paths, might require more thought. + def testExternalContentProxyCopy(self): + self.log("Create external content to local resource") + image_path = self.duplicateImage() + external_headers = { + "Link": "; rel =\"http://fedora.info/definitions/fcrepo#ExternalContent\"; " + "handling=\"copy\"; type=\"image/jpeg\"".format(image_path) + } + r = self.do_post(headers=external_headers) + self.checkResponse(TC.CREATED, r) + location = self.get_location(r) + r = self.do_get(location) + self.checkResponse(TC.OK, r) + self.assertHeaderExists(r, "Content-type", "image/jpeg") + self.log("Delete the local file") + os.unlink(image_path) + r = self.do_get(location) + self.checkResponse(TC.OK, r) - - self.assertNotEqual(first_etag, third_etag) - self.assertNotEqual(second_etag, third_etag) - - def getEtag(self, uri): - r = self.do_head(uri) - self.assertTrue(TC.OK, r) - if "ETag" in r.headers: - return r.headers["ETag"] \ No newline at end of file + # @Test # These tests require Fedora to allow the paths, might require more thought. + def testExternalContentHttpProxy(self): + self.log("Create a resource") + with open(self.getImagePath(), 'rb') as fp: + data = fp.read() + r = self.do_post(headers={ + "Content-type": "image/jpeg" + }, body=data) + self.checkResponse(TC.CREATED, r) + external_location = self.get_location(r) + self.log("Create external content to http resource") + external_headers = { + "Link": "<{}>; rel =\"http://fedora.info/definitions/fcrepo#ExternalContent\"; " + "handling=\"proxy\"; type=\"image/jpeg\"".format(external_location) + } + r = self.do_post(headers=external_headers) + self.checkResponse(TC.CREATED, r) + location = self.get_location(r) + r = self.do_get(location) + self.checkResponse(TC.OK, r) + self.log("Delete the local file") diff --git a/fixity_tests.py b/fixity_tests.py index 5c74177..9058d64 100644 --- a/fixity_tests.py +++ b/fixity_tests.py @@ -30,7 +30,7 @@ def aFixityTest(self): headers = { 'Content-type': 'image/jpeg', } - with open(os.path.join(os.getcwd(), 'resources', 'basic_image.jpg'), 'rb') as fp: + with open(self.getImagePath(), 'rb') as fp: data = fp.read() r = self.do_post(self.getBaseUri(), headers=headers, body=data) self.assertEqual(201, r.status_code, 'Did not create binary') diff --git a/testrunner.py b/testrunner.py index 9d027df..e85ba30 100755 --- a/testrunner.py +++ b/testrunner.py @@ -36,13 +36,26 @@ class FedoraTestRunner: (TestConstants.LOG_FILE_PARAM, False), (TestConstants.SELECTED_TESTS_PARAM, False), (TestConstants.SOLR_URL_PARAM, False), - (TestConstants.TRIPLESTORE_URL_PARAM, False) + (TestConstants.TRIPLESTORE_URL_PARAM, False), + ('debug_level', False), ] config = {} logger = None - def set_up(self, args): - self.parse_cmdline_args(args) + tests = { + 'basic': FedoraBasicIxnTests, + 'version': FedoraVersionTests, + 'fixity': FedoraFixityTests, + 'rdf': FedoraRdfTests, + 'sparql': FedoraSparqlTests, + 'transaction': FedoraTransactionTests, + 'authz': FedoraAuthzTests, + 'indirect': FedoraIndirectTests, + 'search': FedoraSimpleSearchTests, + } + + def set_up(self, setup_args): + self.parse_cmdline_args(setup_args) self.check_config() def load_config(self, file, site): @@ -52,7 +65,7 @@ def load_config(self, file, site): yml = load(fp.read(), Loader=SafeLoader) self.config = yml.get(site) - def parse_cmdline_args(self, args): + def parse_cmdline_args(self, command_args): filename = eval('args.' + TestConstants.CONFIG_FILE_PARAM) if filename is not None: sitename = eval('args.' + TestConstants.SITE_NAME_PARAM) @@ -78,42 +91,39 @@ def check_config(self): if param_status: raise Exception("Missing config parameter (" + param_name + ")") - def run_tests(self): - for test in self.config[TestConstants.SELECTED_TESTS_PARAM]: - if test == 'all' or test == 'basic': - nested = FedoraBasicIxnTests(self.config) - nested.run_tests() - if test == 'all' or test == 'version': - versioning = FedoraVersionTests(self.config) - versioning.run_tests() - if test == 'all' or test == 'fixity': - fixity = FedoraFixityTests(self.config) - fixity.run_tests() - if test == 'all' or test == 'rdf': - rdf = FedoraRdfTests(self.config) - rdf.run_tests() - if test == 'all' or test == 'sparql': - sparql = FedoraSparqlTests(self.config) - sparql.run_tests() - if test == 'all' or test == 'transaction': - transaction = FedoraTransactionTests(self.config) - transaction.run_tests() - if test == 'all' or test == 'authz': - authz = FedoraAuthzTests(self.config) - authz.run_tests() - if test == 'all' or test == 'indirect': - indirect = FedoraIndirectTests(self.config) - indirect.run_tests() - if test == 'all' or test == 'search': - search = FedoraSimpleSearchTests(self.config) - search.run_tests() - if test == 'camel': - camel = FedoraCamelTests(self.config) - camel.run_tests() - - def main(self, args): - self.set_up(args) - self.run_tests() + def run_tests(self) -> dict: + results = {} + for chosen_test in self.config[TestConstants.SELECTED_TESTS_PARAM]: + if chosen_test == 'all': + for test_param, test_class in self.tests.items(): + instance = test_class(self.config) + instance.run_tests() + results[instance.__class__.__name__] = instance.results + if chosen_test == 'camel': + camel = FedoraCamelTests(self.config) + camel.run_tests() + else: + test_class = self.tests[chosen_test] + instance = test_class(self.config) + instance.run_tests() + results[instance.__class__.__name__] = instance.results + return results + + @staticmethod + def report(test_results: dict): + """ Print out the test results collected from the various tests. """ + if len(test_results) > 0: + print("\n{:-^50}".format("Test Results")) + for k, results in test_results.items(): + print(f"\nTest Class: {k}") + for method, result in results.items(): + print(f" Method: {method}, Result: ", end="") + print("Pass") if result['result'] else print(f"Failed, {result['message']}") + + def main(self, application_args): + self.set_up(application_args) + results = self.run_tests() + self.report(results) def csv_list(string): @@ -172,6 +182,11 @@ def __call__(self, parser, args, values, option_string=None): parser.add_argument('-t', '--tests', dest="selected_tests", help='Comma separated list of which tests to run from ' '{0}. Defaults to running all tests'.format(", ".join(CSVAction.valid_options)), default=['all'], type=csv_list, action=CSVAction) + group = parser.add_mutually_exclusive_group() + group.add_argument('-v', dest='debug_level', action='store_const', const=1, default=0, + help='Set verbosity to level 1') + group.add_argument('-vv', dest='debug_level', action='store_const', const=2, default=0, + help='Set verbosity to level 2') args = parser.parse_args() tests = FedoraTestRunner() diff --git a/version_tests.py b/version_tests.py index 05e4e4c..07e2042 100644 --- a/version_tests.py +++ b/version_tests.py @@ -7,7 +7,7 @@ from rdflib.namespace import DC, RDF -def getVersionEndpoint(uri): +def get_version_endpoint(uri): if not uri.endswith("/" + TestConstants.FCR_VERSIONS): uri += "/" + TestConstants.FCR_VERSIONS return uri @@ -32,7 +32,7 @@ def count_mementos(response): def checkMementoCount(self, expected, uri, admin=None): if admin is None: admin = True - uri = getVersionEndpoint(uri) + uri = get_version_endpoint(uri) headers = { 'Accept': TestConstants.LINK_FORMAT_MIMETYPE } @@ -43,7 +43,7 @@ def checkMementoCount(self, expected, uri, admin=None): def getNthMemento(self, uri, memento_number=1, admin=None): if admin is None: admin = True - uri = getVersionEndpoint(uri) + uri = get_version_endpoint(uri) headers = { 'Accept': TestConstants.LINK_FORMAT_MIMETYPE } @@ -58,7 +58,7 @@ def getNthMemento(self, uri, memento_number=1, admin=None): self.fail("Count not get the {} memento, only found {}".format(memento_number+1, len(mementos))) # Waiting on https://fedora-repository.atlassian.net/browse/FCREPO-3655 - # @Test + @Test def doContainerVersioningTest(self): headers = { 'Link': self.make_type(TestConstants.LDP_BASIC) @@ -164,11 +164,13 @@ def doBinaryVersioningTest(self): r = self.do_get(description_version_endpoint) self.checkResponse(TestConstants.OK, r) + self.log("Wait for one second") + time.sleep(1) self.log("Create a version") r = self.do_post(version_endpoint) self.checkResponse(TestConstants.CREATED, r) - self.log("Try to create another version too quickly") + self.log("Try to create another version within a second") r = self.do_post(version_endpoint) self.checkResponse(TestConstants.CREATED, r) @@ -178,9 +180,11 @@ def doBinaryVersioningTest(self): r = self.do_post(description_version_endpoint) self.checkResponse(TestConstants.CREATED, r) - self.checkMementoCount(4, location) - self.checkMementoCount(4, description_location) + self.checkMementoCount(2, location) + self.checkMementoCount(2, description_location) + self.log("Wait one second") + time.sleep(1) new_date = FedoraTests.get_rfc_date("2000-06-01 08:21:00") self.log("Try to create a version with provided datetime") @@ -220,7 +224,7 @@ def doBinaryVersioningTest(self): self.checkResponse(TestConstants.CREATED, r) self.log("Count mementos") - self.checkMementoCount(7, version_endpoint) + self.checkMementoCount(4, version_endpoint) self.log("Try to DELETE the memento") r = self.do_delete(memento_location) @@ -231,7 +235,7 @@ def doBinaryVersioningTest(self): self.checkResponse(TestConstants.OK, r) self.log("Validate count of mementos again") - self.checkMementoCount(7, version_endpoint) + self.checkMementoCount(4, version_endpoint) @Test def checkBinaryVersioning(self): @@ -264,6 +268,9 @@ def checkBinaryVersioning(self): self.checkResponse(200, r) self.checkValue(1, self.count_mementos(r)) + self.log("Wait a second") + time.sleep(1) + self.log("Create version of binary from existing") r = self.do_post(binary_versions) self.checkResponse(201, r) From e1f552782186e79809cf7c7632b82a18919e6de9 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 3 Mar 2023 17:04:26 -0600 Subject: [PATCH 14/20] Clean up sparql and remove second debug level --- sparql_tests.py | 24 ++++++++++++------------ testrunner.py | 7 ++----- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/sparql_tests.py b/sparql_tests.py index 49567e7..4403377 100644 --- a/sparql_tests.py +++ b/sparql_tests.py @@ -26,7 +26,7 @@ def doSparqlContainerTest(self): 'Content-type': 'text/turtle' } r = self.do_post(self.getBaseUri(), headers=headers, body=TC.OBJECT_TTL) - self.assertEqual(201, r.status_code, "Did not create container") + self.checkResponse(201, r) location = self.get_location(r) self.log("Set dc:title with SPARQL") @@ -34,12 +34,12 @@ def doSparqlContainerTest(self): 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=patch_headers, body=self.TITLE_SPARQL) - self.assertEqual(204, r.status_code, "Did not get expected result") + self.checkResponse(204, r) self.assertTitleExists("First title", location) self.log("Update dc:title with SPARQL") r = self.do_patch(location, headers=patch_headers, body=self.UPDATE_SPARQL) - self.assertEqual(204, r.status_code, "Did not get expected results") + self.checkResponse(204, r) self.assertTitleExists("Updated title", location) @Test @@ -51,7 +51,7 @@ def doSparqlBinaryTest(self): with open(os.path.join(os.getcwd(), 'resources', 'basic_image.jpg'), 'rb') as fp: data = fp.read() r = self.do_post(self.getBaseUri(), headers=headers, body=data) - self.assertEqual(201, r.status_code, "Did not get expected response") + self.checkResponse(201, r) description = self.find_binary_description(r) self.log("Set dc:title with SPARQL") @@ -59,19 +59,19 @@ def doSparqlBinaryTest(self): 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(description, headers=patch_headers, body=self.TITLE_SPARQL) - self.assertEqual(204, r.status_code, "Did not get expected result") + self.checkResponse(204, r) self.assertTitleExists("First title", description) self.log("Update dc:title with SPARQL") r = self.do_patch(description, headers=patch_headers, body=self.UPDATE_SPARQL) - self.assertEqual(204, r.status_code, "Did not get expected results") + self.checkResponse(204, r) self.assertTitleExists("Updated title", description) @Test def doUnicodeSparql(self): self.log("Create a container") r = self.do_post(self.getBaseUri()) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) location = self.get_location(r) sparql = "PREFIX dc: " \ @@ -83,14 +83,14 @@ def doUnicodeSparql(self): 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=headers, body=sparql) - self.assertEqual(204, r.status_code, "Did not get expected response code") + self.checkResponse(204, r) self.assertTitleExists("Die von Blumenbach gegründete anthropologische Sammlung der Universität", location) @Test def doAddType(self): self.log("Create a container") r = self.do_post(self.getBaseUri()) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) location = self.get_location(r) sparql = "PREFIX rdf: " \ @@ -102,14 +102,14 @@ def doAddType(self): 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=headers, body=sparql) - self.assertEqual(204, r.status_code, "Did not get expected response code") + self.checkResponse(204, r) self.assertTypeExists("http://www.example.org/ns#type", location) @Test def doAddRestrictedType(self): self.log("Create a container") r = self.do_post(self.getBaseUri()) - self.assertEqual(201, r.status_code, "Did not get expected response code") + self.checkResponse(201, r) location = self.get_location(r) sparql = "PREFIX rdf: " \ @@ -121,7 +121,7 @@ def doAddRestrictedType(self): 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } r = self.do_patch(location, headers=headers, body=sparql) - self.assertEqual(409, r.status_code, "Did not get expected response code") + self.checkResponse(409, r) @Test def doInboundReferenceContainer(self): diff --git a/testrunner.py b/testrunner.py index e85ba30..a6db4d3 100755 --- a/testrunner.py +++ b/testrunner.py @@ -182,11 +182,8 @@ def __call__(self, parser, args, values, option_string=None): parser.add_argument('-t', '--tests', dest="selected_tests", help='Comma separated list of which tests to run from ' '{0}. Defaults to running all tests'.format(", ".join(CSVAction.valid_options)), default=['all'], type=csv_list, action=CSVAction) - group = parser.add_mutually_exclusive_group() - group.add_argument('-v', dest='debug_level', action='store_const', const=1, default=0, - help='Set verbosity to level 1') - group.add_argument('-vv', dest='debug_level', action='store_const', const=2, default=0, - help='Set verbosity to level 2') + parser.add_argument('-v', dest='debug_level', action='store_const', const=1, default=0, + help='Set verbosity to level 1') args = parser.parse_args() tests = FedoraTestRunner() From 651574007fdef59e4c4e1033fcc9c5cc7e7ad217 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 1 Dec 2023 12:10:23 -0600 Subject: [PATCH 15/20] Add ArchivalGroup tests --- TestConstants.py | 6 +- abstract_fedora_tests.py | 21 ++-- archival_group_tests.py | 190 ++++++++++++++++++++++++++++++++++ authz_tests.py | 69 +++++++------ basic_interaction_tests.py | 159 ++++++++++++++++++++++++----- testrunner.py | 36 +++++-- transaction_tests.py | 203 ++++++++++++++++++++++++++++++++++--- 7 files changed, 595 insertions(+), 89 deletions(-) create mode 100644 archival_group_tests.py diff --git a/TestConstants.py b/TestConstants.py index 042d58e..5e4ceb6 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -30,7 +30,9 @@ SERVER_MANAGED = FEDORA_NS + "ServerManaged" INBOUND_REFERENCE = FEDORA_API_NS + "PreferInboundReferences" EMBEDED_RESOURCE = FEDORA_NS + "EmbedResources" -ATOMIC_ID_HEADER="Atomic-ID" +ATOMIC_ID_HEADER = "Atomic-ID" +ARCHIVAL_GROUP = FEDORA_NS + "ArchivalGroup" +OVERWRITE_TOMBSTONE_HEADER = "Overwrite-Tombstone" FEDORA_TX_NS = "http://fedora.info/definitions/v4/transaction#" FEDORA_TX_ENDPOINT_REL = FEDORA_TX_NS + "endpoint" @@ -87,6 +89,8 @@ CREATED = 201 NO_CONTENT = 204 OK = 200 +NOT_AUTHORIZED = 401 +FORBIDDEN = 403 NOT_FOUND = 404 CONFLICT = 409 GONE = 410 diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index 71b1df3..318b70e 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -196,9 +196,9 @@ def assert_regex_matches(self, pattern, text, msg): self.fail(self._formatMessage(msg, standard_msg)) @staticmethod - def make_type(type): + def make_type(link_type: str) -> str: """ Turn a URI to Link type format """ - return "<{0}>; rel=\"type\"".format(type) + return "<{0}>; rel=\"type\"".format(link_type) def tear_down(self): """ Delete any resources created """ @@ -206,7 +206,7 @@ def tear_down(self): self.cleanup(node) self.nodes.clear() - def cleanup(self, uri): + def cleanup(self, uri: str) -> bool: """ Remove the CONTAINER """ self.log("Deleting {0}".format(uri)) r = self.do_delete(uri) @@ -223,7 +223,7 @@ def cleanup(self, uri): return True return False - def check_for_retest(self, uri): + def check_for_retest(self, uri: str): """Try to create CONTAINER """ try: response = self.do_put(uri) @@ -249,14 +249,14 @@ def check_for_retest(self, uri): "in the configuration.") quit() - def getHeader(self, uri, header_name): + def getHeader(self, uri: str, header_name: str): r = self.do_head(uri) self.assertTrue(TestConstants.OK, r) if header_name in r.headers: return r.headers[header_name] @staticmethod - def get_link_headers(response): + def get_link_headers(response: requests.Response) -> dict: """ Get the response's LINK headers, returned as a dict of key -> list() where the key is the rel=property and the list contains all uris """ headers = {} @@ -368,8 +368,13 @@ def find_binary_description(self, response): self.assertIsNotNone(headers['describedby']) return headers['describedby'][0] - def checkResponse(self, expected, response): - self.checkValue(expected, response.status_code) + def checkResponse(self, expected, response: requests.Response): + try: + str_expected = [str(x) for x in expected] + all_expect = " or ".join(str_expected) + self.assertIn(response.status_code, expected, f"Expected ({all_expect}) got {response.status_code}") + except TypeError: + self.checkValue(expected, response.status_code) def checkValue(self, expected, received): self.assertEqual(expected, received, f"Expected {expected} but received {received}") diff --git a/archival_group_tests.py b/archival_group_tests.py new file mode 100644 index 0000000..6243b82 --- /dev/null +++ b/archival_group_tests.py @@ -0,0 +1,190 @@ +#!/bin/env python + +import TestConstants as TC +from abstract_fedora_tests import FedoraTests, register_tests, Test + + +@register_tests +class FedoraArchivalGroupTests(FedoraTests): + # Create test objects all inside here for easy of review + CONTAINER = "/test_archival_groups" + + @Test + def testCreateArchivalGroup(self): + self.log("Create Archival Group container") + r = self.do_post(self.getBaseUri(), headers={ + "Link": self.make_type(TC.ARCHIVAL_GROUP) + }) + container_location = self.get_location(r) + self.checkResponse(TC.CREATED, r) + self.log("Create archivalgroup member") + r = self.do_post(container_location) + self.checkResponse(TC.CREATED, r) + + @Test + def testDeleteArchivalGroupMember(self): + self.log("Create Archival Group container") + r = self.do_post(self.getBaseUri(), headers={ + "Link": self.make_type(TC.ARCHIVAL_GROUP) + }) + container_location = self.get_location(r) + self.checkResponse(TC.CREATED, r) + self.log("Create archivalgroup member") + r = self.do_post(container_location) + self.checkResponse(TC.CREATED, r) + member_location = self.get_location(r) + r = self.do_get(member_location) + self.checkResponse(TC.OK, r) + self.log("Delete archivalgroup member") + r = self.do_delete(member_location) + self.checkResponse(TC.NO_CONTENT, r) + self.log("Verify archivalgroup member is gone") + r = self.do_get(member_location) + self.checkResponse(TC.GONE, r) + + self.log("Try to delete archivalgroup member tombstone") + r = self.do_delete(member_location + "/" + TC.FCR_TOMBSTONE) + self.checkResponse(TC.METHOD_NOT_ALLOWED, r) + + @Test + def putOverArchivalGroupMember(self): + self.log("Create Archival Group container") + r = self.do_post(self.getBaseUri(), headers={ + "Link": self.make_type(TC.ARCHIVAL_GROUP) + }) + container_location = self.get_location(r) + self.checkResponse(TC.CREATED, r) + self.log("Create archivalgroup member") + r = self.do_post(container_location) + self.checkResponse(TC.CREATED, r) + member_location = self.get_location(r) + r = self.do_get(member_location) + self.checkResponse(TC.OK, r) + self.log("Delete archivalgroup member") + r = self.do_delete(member_location) + self.checkResponse(TC.NO_CONTENT, r) + self.log("Verify archivalgroup member is gone") + r = self.do_get(member_location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over archivalgroup member tombstone, without header") + r = self.do_put(member_location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over archivalgroup member tombstone, with header") + r = self.do_put(member_location, headers={ + TC.OVERWRITE_TOMBSTONE_HEADER: "true" + }) + self.checkResponse(TC.CREATED, r) + + @Test + def putOverArchivalGroupMemberWithDifferentType(self): + self.log("Create Archival Group container") + r = self.do_post(self.getBaseUri(), headers={ + "Link": self.make_type(TC.ARCHIVAL_GROUP) + }) + container_location = self.get_location(r) + self.checkResponse(TC.CREATED, r) + self.log("Create archivalgroup member") + r = self.do_post(container_location) + self.checkResponse(TC.CREATED, r) + member_location = self.get_location(r) + r = self.do_get(member_location) + self.checkResponse(TC.OK, r) + self.log("Delete archivalgroup member") + r = self.do_delete(member_location) + self.checkResponse(TC.NO_CONTENT, r) + self.log("Verify archivalgroup member is gone") + r = self.do_get(member_location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over archivalgroup member tombstone, without header") + r = self.do_put(member_location, headers={ + "Link": self.make_type(TC.LDP_NON_RDF_SOURCE), + "Content-type": "text/plain" + }, body="Hello World!") + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over archivalgroup member tombstone, with header") + r = self.do_put(member_location, headers={ + "Link": self.make_type(TC.LDP_NON_RDF_SOURCE), + "Content-type": "text/plain", + TC.OVERWRITE_TOMBSTONE_HEADER: "true" + }, body="Hello World!") + self.checkResponse(TC.CONFLICT, r) + + @Test + def testCreateAndDeleteArchivalGroup(self): + self.log("Create Archival Group container") + r = self.do_post(self.getBaseUri(), headers={ + "Link": self.make_type(TC.ARCHIVAL_GROUP) + }) + container_location = self.get_location(r) + self.checkResponse(TC.CREATED, r) + self.log("Create archivalgroup member") + r = self.do_post(container_location) + self.checkResponse(TC.CREATED, r) + member_location = self.get_location(r) + r = self.do_get(member_location) + self.checkResponse(TC.OK, r) + self.log("Delete archivalgroup") + r = self.do_delete(container_location) + self.checkResponse(TC.NO_CONTENT, r) + self.log("Verify archivalgroup is gone") + r = self.do_get(container_location) + self.checkResponse(TC.GONE, r) + + self.log("Try to delete archivalgroup tombstone") + r = self.do_delete(container_location + "/" + TC.FCR_TOMBSTONE) + self.checkResponse(TC.NO_CONTENT, r) + + self.log("Check for archivalgroup") + r = self.do_get(container_location) + self.checkResponse(TC.NOT_FOUND, r) + self.log("Check for archivalgroup member") + r = self.do_get(member_location) + self.checkResponse(TC.NOT_FOUND, r) + + @Test + def testCreateAndPutOverArchivalGroup(self): + self.log("Create Archival Group container") + r = self.do_post(self.getBaseUri(), headers={ + "Link": self.make_type(TC.ARCHIVAL_GROUP) + }) + container_location = self.get_location(r) + self.checkResponse(TC.CREATED, r) + self.log("Create archivalgroup member") + r = self.do_post(container_location) + self.checkResponse(TC.CREATED, r) + member_location = self.get_location(r) + r = self.do_get(member_location) + self.checkResponse(TC.OK, r) + + self.log("Try to delete archivalgroup") + r = self.do_delete(container_location) + self.checkResponse(TC.NO_CONTENT, r) + r = self.do_get(container_location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over archivalgroup, without header") + r = self.do_put(container_location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT a normal RDFResource over archivalgroup, with header") + r = self.do_put(container_location, headers={ + TC.OVERWRITE_TOMBSTONE_HEADER: "true" + }) + # TODO: This should not be a SERVER ERROR, but it could be a CONFLICT + self.checkResponse(TC.SERVER_ERROR, r) + + # TODO: If the above succeeds then this should all be removed. + self.log("Try to PUT an archivalgroup over the archivalgroup, with header") + r = self.do_put(container_location, headers={ + TC.OVERWRITE_TOMBSTONE_HEADER: "true", + "Link": self.make_type(TC.ARCHIVAL_GROUP) + }) + self.checkResponse(TC.CREATED, r) + r = self.do_get(container_location) + self.checkResponse(TC.OK, r) + headers = self.get_link_headers(r) + self.assertNotIn(TC.ARCHIVAL_GROUP, headers['type']) diff --git a/authz_tests.py b/authz_tests.py index 48ab954..a0ae6c1 100644 --- a/authz_tests.py +++ b/authz_tests.py @@ -33,7 +33,7 @@ def verifyAuthEnabled(self): temp_auth = FedoraTests.create_auth(random_string, random_string) r = self.do_get(self.getFedoraBase(), admin=temp_auth) - if 401 != r.status_code: + if TC.NOT_AUTHORIZED != r.status_code: self.log("It appears that authentication is not enabled on your repository.") quit() @@ -50,8 +50,11 @@ def doAuthTests(self): self.verifyAuthEnabled() self.log("Create \"cover\" container") - r = self.do_put(self.getBaseUri() + "/cover") - self.checkResponse(201, r) + r = self.do_post(headers={ + 'Slug': 'cover', + 'Content-type': 'text/turtle' + }, body="@prefix pcdm: .\n <> a pcdm:Object .") + self.checkResponse(TC.CREATED, r) cover_location = self.get_location(r) cover_acl = FedoraAuthzTests.getAclUri(r) @@ -67,7 +70,7 @@ def doAuthTests(self): self.log("Verify no current ACL") r = self.do_get(cover_acl) - self.checkResponse(404, r) + self.checkResponse(TC.NOT_FOUND, r) self.log("Add ACL to \"cover\"") headers = { @@ -75,51 +78,51 @@ def doAuthTests(self): } body = self.COVER_ACL.format(cover_location, self.config[TC.USER_NAME_PARAM]) r = self.do_put(cover_acl, headers=headers, body=body) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) self.log("Create \"files\" inside \"cover\"") r = self.do_put(cover_location + "/files") - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) files_location = self.get_location(r) files_acl = FedoraAuthzTests.getAclUri(r) self.log("Anonymous can't access \"cover\"") r = self.do_get(cover_location, admin=None) - self.checkResponse(401, r) + self.checkResponse(TC.NOT_AUTHORIZED, r) self.log("Anonymous can't access \"cover/files\"") r = self.do_get(files_location, admin=None) - self.checkResponse(401, r) + self.checkResponse(TC.NOT_AUTHORIZED, r) self.log("{0} can access \"cover\"".format(self.config[TC.ADMIN_USER_PARAM])) r = self.do_get(cover_location) - self.checkResponse(200, r) + self.checkResponse(TC.OK, r) self.log("{0} can access \"cover/files\"".format(self.config[TC.ADMIN_USER_PARAM])) r = self.do_get(files_location) - self.checkResponse(200, r) + self.checkResponse(TC.OK, r) self.log("{0} can access \"cover\"".format(self.config[TC.USER_NAME_PARAM])) r = self.do_get(cover_location, admin=False) - self.checkResponse(200, r) + self.checkResponse(TC.OK, r) self.log("{0} can access \"cover/files\"".format(self.config[TC.USER_NAME_PARAM])) r = self.do_get(files_location, admin=False) - self.checkResponse(200, r) + self.checkResponse(TC.OK, r) auth = self.create_auth(self.config[TC.USER2_NAME_PARAM], self.config[TC.USER2_PASS_PARAM]) self.log("{0} can't access \"cover\"".format(self.config[TC.USER2_NAME_PARAM])) r = self.do_get(cover_location, admin=auth) - self.checkResponse(403, r) + self.checkResponse(TC.FORBIDDEN, r) self.log("{0} can't access \"cover/files\"".format(self.config[TC.USER2_NAME_PARAM])) r = self.do_get(files_location, admin=auth) - self.checkResponse(403, r) + self.checkResponse(TC.FORBIDDEN, r) self.log("Verify \"cover/files\" has no ACL") r = self.do_get(files_acl) - self.checkResponse(404, r) + self.checkResponse(TC.NOT_FOUND, r) self.log("PUT Acl to \"cover/files\" to allow access for {0}".format(self.config[TC.USER2_NAME_PARAM])) headers = { @@ -127,15 +130,15 @@ def doAuthTests(self): } body = self.FILES_ACL.format(files_location, self.config[TC.USER2_NAME_PARAM]) r = self.do_put(files_acl, headers=headers, body=body) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) self.log("{0} can't access \"cover\"".format(self.config[TC.USER2_NAME_PARAM])) r = self.do_get(cover_location, admin=auth) - self.checkResponse(403, r) + self.checkResponse(TC.FORBIDDEN, r) self.log("{0} can access \"cover/files\"".format(self.config[TC.USER2_NAME_PARAM])) r = self.do_get(files_location, admin=auth) - self.checkResponse(200, r) + self.checkResponse(TC.OK, r) @Test def doDirectIndirectAuthTests(self): @@ -143,13 +146,13 @@ def doDirectIndirectAuthTests(self): self.log("Create a target container") r = self.do_post() - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) target_location = self.get_location(r) target_acl = FedoraAuthzTests.getAclUri(r) self.log("Create a write container") r = self.do_post() - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) write_location = self.get_location(r) write_acl = FedoraAuthzTests.getAclUri(r) @@ -164,7 +167,7 @@ def doDirectIndirectAuthTests(self): 'Content-type': 'text/turtle' } r = self.do_put(target_acl, headers=headers, body=target_ttl) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) self.log("Make sure the write resource is writable by \"{0}\"".format(self.config[TC.USER_NAME_PARAM])) write_ttl = "@prefix acl: <{2}> .\n" \ @@ -175,23 +178,23 @@ def doDirectIndirectAuthTests(self): " acl:default <{1}> .\n".format(self.config[TC.USER_NAME_PARAM], write_location, TC.ACL_NS) r = self.do_put(write_acl, headers=headers, body=write_ttl) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) self.log("Verify that \"{0}\" can create a simple resource under write resource (POST)".format( self.config[TC.USER_NAME_PARAM])) r = self.do_post(write_location, admin=False) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) uuid_value = str(uuid.uuid4()) self.log("Verify that \"{0}\" can create a simple resource under write resource (PUT)".format( self.config[TC.USER_NAME_PARAM])) r = self.do_put(write_location + "/" + uuid_value, admin=False) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) self.log("Verify that \"{0}\" CANNOT create a resource under target resource".format( self.config[TC.USER_NAME_PARAM])) r = self.do_post(target_location, admin=False) - self.checkResponse(403, r) + self.checkResponse(TC.FORBIDDEN, r) self.log( "Verify that \"{0}\" CANNOT create direct or indirect containers that reference target resources".format( @@ -205,7 +208,7 @@ def doDirectIndirectAuthTests(self): "<> ldp:membershipResource <{1}> ;\n" \ "ldp:hasMemberRelation test:predicateToCreate .\n".format(TC.LDP_NS, target_location) r = self.do_post(write_location, headers=headers, body=direct_ttl, admin=False) - self.checkResponse(403, r) + self.checkResponse(TC.FORBIDDEN, r) headers = { 'Content-type': 'text/turtle', @@ -217,33 +220,33 @@ def doDirectIndirectAuthTests(self): "ldp:membershipResource <{1}> ;\n" \ "ldp:hasMemberRelation test:predicateToCreate .\n".format(TC.LDP_NS, target_location) r = self.do_post(write_location, headers=headers, body=indirect_ttl, admin=False) - self.checkResponse(403, r) + self.checkResponse(TC.FORBIDDEN, r) self.log("Go ahead and create the indirect and direct containers as admin") r = self.do_post(write_location, headers=headers, body=direct_ttl) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) direct_location = self.get_location(r) r = self.do_post(write_location, headers=headers, body=indirect_ttl) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) indirect_location = self.get_location(r) self.log("Attempt to verify that \"{0}\" can not actually create relationships on the readonly resource via " \ "direct or indirect container".format(self.config[TC.USER_NAME_PARAM])) r = self.do_post(direct_location, admin=False) - self.assertEqual(403, r.status_code, "Did not get expected status code") + self.checkResponse(TC.FORBIDDEN, r) r = self.do_post(indirect_location, admin=False) - self.assertEqual(403, r.status_code, "Did not get expected status code") + self.checkResponse(TC.FORBIDDEN, r) self.log("Verify that \"{0}\" can still create a simple resource under write resource (POST)".format( self.config[TC.USER_NAME_PARAM])) r = self.do_post(write_location, admin=False) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) uuid_value = str(uuid.uuid4()) self.log("Verify that \"{0}\" can still create a simple resource under write resource (PUT)".format( self.config[TC.USER_NAME_PARAM])) r = self.do_put(write_location + "/" + uuid_value, admin=False) - self.checkResponse(201, r) + self.checkResponse(TC.CREATED, r) @Test def multipleAuthzCreatePermissiveSet(self): diff --git a/basic_interaction_tests.py b/basic_interaction_tests.py index 8cc9fb0..af16da7 100644 --- a/basic_interaction_tests.py +++ b/basic_interaction_tests.py @@ -61,41 +61,41 @@ def testDeleteAResource(self): self.log("Create container") r = self.do_post(self.getBaseUri()) container_location = self.get_location(r) - self.assertEqual(201, r.status_code, "Did not get expected response") + self.checkResponse(TC.CREATED, r) self.log("Check container exists") r = self.do_head(container_location) - self.assertEqual(200, r.status_code, "Did not get expected response") + self.checkResponse(TC.OK, r) r = self.do_get(container_location) - self.assertEqual(200, r.status_code, "Did not get expected response") + self.checkResponse(TC.OK, r) self.log("Delete the container") r = self.do_delete(container_location) - self.assertEqual(204, r.status_code, "Did not get expected response") + self.checkResponse(TC.NO_CONTENT, r) self.log("Check container doesn't exists") r = self.do_head(container_location) - self.assertEqual(410, r.status_code, "Did not get expected response") + self.checkResponse(TC.GONE, r) r = self.do_get(container_location) - self.assertEqual(410, r.status_code, "Did not get expected response") + self.checkResponse(TC.GONE, r) self.log("Try to put to location held by tombstone") r = self.do_put(container_location) - self.assertEqual(410, r.status_code, "Did not get expected response") + self.checkResponse(TC.GONE, r) self.log("Delete the tombstone") r = self.do_delete(container_location + "/" + TC.FCR_TOMBSTONE) - self.assertEqual(204, r.status_code, "Did not get expected response") + self.checkResponse(TC.NO_CONTENT, r) self.log("Check container doesn't exists") r = self.do_head(container_location) - self.assertEqual(404, r.status_code, "Did not get expected response") + self.checkResponse(TC.NOT_FOUND, r) r = self.do_get(container_location) - self.assertEqual(404, r.status_code, "Did not get expected response") + self.checkResponse(TC.NOT_FOUND, r) self.log("Try to put to location again") r = self.do_put(container_location) - self.assertEqual(201, r.status_code, "Did not get expected response") + self.checkResponse(TC.CREATED, r) @Test def testBasicContainer(self): @@ -122,7 +122,7 @@ def testLdpResource(self): 'Link': link_type } r = self.do_post(self.getBaseUri(), headers=headers) - self.assertEqual(400, r.status_code, "Did not get expected response") + self.checkResponse(TC.BAD_REQUEST, r) @Test def testLdpContainer(self): @@ -132,18 +132,18 @@ def testLdpContainer(self): 'Link': link_type } r = self.do_post(self.getBaseUri(), headers=headers) - self.assertEqual(400, r.status_code, "Did create container") + self.checkResponse(TC.BAD_REQUEST, r) @Test def doNestedTests(self): self.log("Create a container") r = self.createBasicContainer(self.getBaseUri()) - self.assertEqual(201, r.status_code, "Did not get expected status code") + self.checkResponse(TC.CREATED, r) location = self.get_location(r) self.log("Create a container in a container") r = self.createBasicContainer(location) - self.assertEqual(201, r.status_code, "Did not get expected status code") + self.checkResponse(TC.CREATED, r) main_child1 = self.get_location(r) self.log("Create binary inside a container inside a container") @@ -153,12 +153,12 @@ def doNestedTests(self): } data = fp.read() r = self.do_post(main_child1, headers=headers, body=data) - self.assertEqual(201, r.status_code, "Did not get expected status code") + self.checkResponse(TC.CREATED, r) binary_location = self.get_location(r) self.log("Create a second child in the top container") r = self.createBasicContainer(location) - self.assertEqual(201, r.status_code, "Did not get expected status code") + self.checkResponse(TC.CREATED, r) main_child2 = self.get_location(r) self.log("Verify containment") @@ -166,7 +166,7 @@ def doNestedTests(self): 'Accept': TC.JSONLD_MIMETYPE } r = self.do_get(location, headers=headers) - self.assertEqual(200, r.status_code, "Can't get the container") + self.checkResponse(TC.OK, r) body = r.content.decode('UTF-8').rstrip('\ny') json_body = json.loads(body) contained = pyjq.all('.[0]."http://www.w3.org/ns/ldp#contains"', json_body) @@ -187,21 +187,21 @@ def doNestedTests(self): self.log("Delete binary") r = self.do_delete(binary_location) - self.assertEqual(204, r.status_code, "Did not get expected status code") + self.checkResponse(TC.NO_CONTENT, r) self.log("Verify its gone") r = self.do_get(binary_location) - self.assertEqual(410, r.status_code, "Did not get expected status code") + self.checkResponse(TC.GONE, r) self.log("Delete container with a container inside it") r = self.do_delete(location) - self.assertEqual(204, r.status_code, "Did not get expected status code") + self.checkResponse(TC.NO_CONTENT, r) self.log("Verify both are gone") r = self.do_get(main_child1) - self.assertEqual(410, r.status_code, "Did not get expected status code") + self.checkResponse(TC.GONE, r) r = self.do_get(location) - self.assertEqual(410, r.status_code, "Did not get expected status code") + self.checkResponse(TC.GONE, r) @Test def testPurgeContainer(self): @@ -306,7 +306,7 @@ def changeIxnModels(self, location, starting_model): } r = self.do_put(location, headers=headers, files=files) - self.assertEqual(result, r.status_code, "Did not get expected response") + self.checkResponse(result, r) @Test def testChangeIxnModel(self): @@ -424,3 +424,114 @@ def testExternalContentHttpProxy(self): r = self.do_get(location) self.checkResponse(TC.OK, r) self.log("Delete the local file") + + @Test + def testDeleteAndPutOverTombstoneRdf(self): + self.log("Create resource") + r = self.do_post() + self.checkResponse(TC.CREATED, r) + location = self.get_location(r) + + self.log("Delete resource") + r = self.do_delete(location) + self.checkResponse(TC.NO_CONTENT, r) + r = self.do_get(location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over tombstone") + r = self.do_put(location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over tombstone, with header") + r = self.do_put(location, headers={ + TC.OVERWRITE_TOMBSTONE_HEADER: "true" + }) + self.checkResponse(TC.CREATED, r) + + @Test + def testDeleteAndPutOverTombstoneNonRdf(self): + self.log("Create NonRdf resource") + r = self.do_post(headers={ + 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE), + 'Content-type': 'text/plain' + }, body="Hello World!") + self.checkResponse(TC.CREATED, r) + location = self.get_location(r) + + self.log("Delete resource") + r = self.do_delete(location) + self.checkResponse(TC.NO_CONTENT, r) + r = self.do_get(location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over tombstone") + r = self.do_put(location, headers={ + 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE), + 'Content-type': 'text/plain' + }, body="New body") + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over tombstone, with header") + r = self.do_put(location, headers={ + 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE), + 'Content-type': 'text/plain', + TC.OVERWRITE_TOMBSTONE_HEADER: "true" + }, body="New body") + self.checkResponse(TC.CREATED, r) + + @Test + def testDeleteAndPutOverTombstoneWrongTypeRdf(self): + self.log("Create resource RDF") + r = self.do_post() + self.checkResponse(TC.CREATED, r) + location = self.get_location(r) + + self.log("Delete resource") + r = self.do_delete(location) + self.checkResponse(TC.NO_CONTENT, r) + r = self.do_get(location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT NonRdfSource over tombstone") + r = self.do_put(location, headers={ + 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE), + 'Content-type': 'text/plain' + }, body="Hello World!") + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over NonRdfSource tombstone, with header") + r = self.do_put(location, headers={ + 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE), + 'Content-type': 'text/plain', + TC.OVERWRITE_TOMBSTONE_HEADER: "true" + }) + self.checkResponse(TC.CONFLICT, r) + + @Test + def testDeleteAndPutOverTombstoneWrongTypeNonRdf(self): + self.log("Create resource NonRDF") + r = self.do_post(headers={ + 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE), + 'Content-type': 'text/plain' + }, body="Hello World!") + self.checkResponse(TC.CREATED, r) + location = self.get_location(r) + + self.log("Delete resource") + r = self.do_delete(location) + self.checkResponse(TC.NO_CONTENT, r) + r = self.do_get(location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT RdfSource over tombstone") + r = self.do_put(location, headers={ + 'Link': self.make_type(TC.LDP_BASIC), + }) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over RdfSource tombstone, with header") + r = self.do_put(location, headers={ + 'Link': self.make_type(TC.LDP_BASIC), + TC.OVERWRITE_TOMBSTONE_HEADER: "true" + }) + self.checkResponse(TC.CONFLICT, r) diff --git a/testrunner.py b/testrunner.py index a6db4d3..82d088c 100755 --- a/testrunner.py +++ b/testrunner.py @@ -20,6 +20,7 @@ from indirect_tests import FedoraIndirectTests from camel_tests import FedoraCamelTests from ssearch_tests import FedoraSimpleSearchTests +from archival_group_tests import FedoraArchivalGroupTests class FedoraTestRunner: @@ -38,6 +39,7 @@ class FedoraTestRunner: (TestConstants.SOLR_URL_PARAM, False), (TestConstants.TRIPLESTORE_URL_PARAM, False), ('debug_level', False), + ('failed_only', False), ] config = {} logger = None @@ -52,6 +54,7 @@ class FedoraTestRunner: 'authz': FedoraAuthzTests, 'indirect': FedoraIndirectTests, 'search': FedoraSimpleSearchTests, + 'archivalgroup': FedoraArchivalGroupTests, } def set_up(self, setup_args): @@ -65,6 +68,9 @@ def load_config(self, file, site): yml = load(fp.read(), Loader=SafeLoader) self.config = yml.get(site) + def get_tests(self) -> list: + return list(self.tests.keys()) + def parse_cmdline_args(self, command_args): filename = eval('args.' + TestConstants.CONFIG_FILE_PARAM) if filename is not None: @@ -109,16 +115,17 @@ def run_tests(self) -> dict: results[instance.__class__.__name__] = instance.results return results - @staticmethod - def report(test_results: dict): + def report(self, test_results: dict): """ Print out the test results collected from the various tests. """ if len(test_results) > 0: print("\n{:-^50}".format("Test Results")) for k, results in test_results.items(): print(f"\nTest Class: {k}") for method, result in results.items(): - print(f" Method: {method}, Result: ", end="") - print("Pass") if result['result'] else print(f"Failed, {result['message']}") + if result['result'] and not self.config['failed_only']: + print(f" Method: {method}, Result: Pass") + elif not result['result']: + print(f" Method: {method}, Result: Failed, {result['message']}") def main(self, application_args): self.set_up(application_args) @@ -137,8 +144,14 @@ def csv_list(string): class CSVAction(argparse.Action): - valid_options = ["authz", "basic", "sparql", "rdf", "version", "transaction", "fixity", "indirect", "camel", - "search"] + """ Holds the valid keys for the csv list """ + valid_options = [] + + def __init__(self, option_strings, dest, **kwargs): + if 'csv_options' in kwargs: + self.valid_options = kwargs['csv_options'] + del kwargs['csv_options'] + super(CSVAction, self).__init__(option_strings, dest, **kwargs) def __call__(self, parser, args, values, option_string=None): if isinstance(values, list): @@ -158,6 +171,7 @@ def __call__(self, parser, args, values, option_string=None): if __name__ == '__main__': + tests = FedoraTestRunner() parser = argparse.ArgumentParser(description="Fedora Tester runs a series of tests against an instance of the " "community implementation of the Fedora API specification.") @@ -180,11 +194,13 @@ def __call__(self, parser, args, values, option_string=None): parser.add_argument('-k', '--' + TestConstants.USER2_PASS_PARAM, dest=TestConstants.USER2_PASS_PARAM, help="Second regular user password") parser.add_argument('-t', '--tests', dest="selected_tests", help='Comma separated list of which tests to run from ' - '{0}. Defaults to running all tests'.format(", ".join(CSVAction.valid_options)), - default=['all'], type=csv_list, action=CSVAction) + '{0}. Defaults to running all tests'.format(", ".join(tests.get_tests())), + default=['all'], type=csv_list, action=CSVAction, csv_options=tests.get_tests()) parser.add_argument('-v', dest='debug_level', action='store_const', const=1, default=0, - help='Set verbosity to level 1') + help='Show all test steps') + parser.add_argument('--failed-only', dest='failed_only', action='store_true', default=False, + help='Only show failed tests') args = parser.parse_args() - tests = FedoraTestRunner() + tests.main(args) diff --git a/transaction_tests.py b/transaction_tests.py index 86d1cfe..8b592ac 100644 --- a/transaction_tests.py +++ b/transaction_tests.py @@ -6,7 +6,6 @@ @register_tests class FedoraTransactionTests(FedoraTests): - # Create test objects all inside here for easy of review CONTAINER = "/test_transaction" @@ -19,7 +18,7 @@ def createTransaction(self, admin=None): self.checkResponse(201, r) return self.get_location(r) - def checkResponse(self, expected, response, tx_id = None): + def checkResponse(self, expected, response, tx_id=None): try: super().checkResponse(expected, response) except AssertionError as e: @@ -234,7 +233,7 @@ def testTransactionExclusion(self): self.log("Get the container") r = self.do_get(container_id) self.checkResponse(TC.OK, r) - + tx_location = self.createTransaction() self.log("Delete the container in the transaction") @@ -268,9 +267,185 @@ def testTransactionExclusion(self): r = self.do_get(container_id) self.checkResponse(TC.OK, r) - ''' Waiting on https://fedora-repository.atlassian.net/browse/FCREPO-3827 ''' - #@Test - def aPlainUserTransactionRollback(self): + @Test + def aPlainUserTransactionRollbackInternal(self): + child, tx_id = self.setup_user_writeable_tx() + + self.log("Rollback transaction") + r = self.do_delete(tx_id, admin=False) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log("Test getting the child (" + child + ") in transaction") + tx_headers = {TC.ATOMIC_ID_HEADER: tx_id} + r = self.do_get(child, admin=False, headers=tx_headers) + self.checkResponse(TC.CONFLICT, r, tx_id) + + self.log("Test getting the child (" + child + ") outside transaction") + r = self.do_get(child, admin=False) + self.checkResponse(TC.NOT_FOUND, r, tx_id) + + @Test + def aPlainUserTransactionRollbackExternal(self): + child, tx_id = self.setup_user_writeable_tx(fedora_base_uri=self.getFedoraBase()) + + self.log("Rollback transaction") + r = self.do_delete(tx_id, admin=False) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log("Test getting the child (" + child + ") in transaction") + tx_headers = {TC.ATOMIC_ID_HEADER: tx_id} + r = self.do_get(child, admin=False, headers=tx_headers) + self.checkResponse(TC.CONFLICT, r, tx_id) + + self.log("Test getting the child (" + child + ") outside transaction") + r = self.do_get(child, admin=False) + self.checkResponse(TC.NOT_FOUND, r, tx_id) + + @Test + def aPlainUserTransactionCommitInternal(self): + child, tx_id = self.setup_user_writeable_tx() + + self.log(f"Test getting the child ({child}) outside the transaction") + r = self.do_get(child, admin=False) + self.checkResponse(TC.NOT_FOUND, r, tx_id) + + self.log("Commit transaction") + r = self.do_put(tx_id, admin=False) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log(f"Test getting the child ({child}) outside the transaction") + r = self.do_get(child, admin=False) + self.checkResponse(TC.OK, r, tx_id) + + @Test + def aPlainUserTransactionCommitExternal(self): + child, tx_id = self.setup_user_writeable_tx(fedora_base_uri=self.getFedoraBase()) + + self.log(f"Test getting the child ({child}) outside the transaction") + r = self.do_get(child, admin=False) + self.checkResponse(TC.NOT_FOUND, r, tx_id) + + self.log("Commit transaction") + r = self.do_put(tx_id, admin=False) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log(f"Test getting the child ({child}) outside the transaction") + r = self.do_get(child, admin=False) + self.checkResponse(TC.OK, r, tx_id) + + @Test + def aSinglePlainUserTransactionCommitInternal(self): + child, tx_id = self.single_user_tx_setup() + + self.log("Commit transaction") + r = self.do_put(tx_id, admin=False) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log(f"Test getting the child ({child}) outside the transaction") + r = self.do_get(child, admin=False) + self.checkResponse(TC.OK, r, tx_id) + + self.log("Try to start a transaction as the second normal user") + tx_endpoint = self.get_transaction_provider() + r = self.do_post(tx_endpoint, admin=self.create_user2_auth()) + self.checkResponse(TC.FORBIDDEN, r) + + @Test + def aSinglePlainUserTransactionCommitExternal(self): + child, tx_id = self.single_user_tx_setup(fedora_base_uri=self.getFedoraBase()) + + self.log("Commit transaction") + r = self.do_put(tx_id, admin=False) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log(f"Test getting the child ({child}) outside the transaction") + r = self.do_get(child, admin=False) + self.checkResponse(TC.OK, r, tx_id) + + self.log("Try to start a transaction as the second normal user") + tx_endpoint = self.get_transaction_provider() + r = self.do_post(tx_endpoint, admin=self.create_user2_auth()) + self.checkResponse(TC.FORBIDDEN, r) + + @Test + def aSinglePlainUserTransactionCommitExternal(self): + child, tx_id = self.single_user_tx_setup(fedora_base_uri=self.getFedoraBase()) + + self.log("Commit transaction") + r = self.do_put(tx_id, admin=False) + self.checkResponse(TC.NO_CONTENT, r, tx_id) + + self.log(f"Test getting the child ({child}) outside the transaction") + r = self.do_get(child, admin=False) + self.checkResponse(TC.OK, r, tx_id) + + self.log("Try to start a transaction as the second normal user") + tx_endpoint = self.get_transaction_provider() + r = self.do_post(tx_endpoint, admin=self.create_user2_auth()) + self.checkResponse(TC.FORBIDDEN, r) + + def single_user_tx_setup(self, fedora_base_uri="info:fedora"): + """ Setup a root ACL with only one normal user allowed to access transactions. """ + root_acl = "@prefix rdfs: .\n" \ + "@prefix acl: .\n" \ + "@prefix foaf: .\n" \ + "@prefix fedora: .\n" \ + "@prefix webac: .\n" \ + "\n" \ + "<{1}/fcr:acl> a webac:Acl .\n" \ + "\n" \ + "<{1}/fcr:acl#authz> a acl:Authorization ;\n" \ + " rdfs:label \"Root Authorization\" ;\n" \ + " rdfs:comment \"By default, all non-Admin agents (foaf:Agent) only " \ + " have read access (acl:Read) to the repository\" ;\n" \ + " acl:agentClass foaf:Agent ;\n" \ + " acl:mode acl:Read ;\n" \ + " acl:accessTo ;\n" \ + " acl:default .\n" \ + "\n" \ + "<{1}/fcr:tx#authz_read_write> a acl:Authorization ;\n" \ + " rdfs:label \"Test Tx Authorization\" ;\n" \ + " rdfs:comment \"Provide read write access to the transaction endpoint\" ;\n" \ + " acl:agent \"{0}\" ;\n" \ + " acl:mode acl:Read, acl:Write ;\n" \ + " acl:accessTo ;\n" \ + " acl:default .\n".format(self.config[TC.USER_NAME_PARAM], + fedora_base_uri) + return self.setup_user_writeable_tx(root_acl=root_acl) + + def setup_user_writeable_tx(self, root_acl=None, fedora_base_uri: str = "info:fedora") -> tuple: + """ Setup the needed containers and ACLs for the tests. """ + self.log("Update the root ACL to allow {0} to access transactions".format(self.config[TC.USER_NAME_PARAM])) + + if root_acl is None: + root_acl = "@prefix rdfs: .\n" \ + "@prefix acl: .\n" \ + "@prefix foaf: .\n" \ + "@prefix fedora: .\n" \ + "@prefix webac: .\n" \ + "\n" \ + "<{0}/fcr:acl> a webac:Acl .\n" \ + "\n" \ + "<{0}/fcr:acl#authz> a acl:Authorization ;\n" \ + " rdfs:label \"Root Authorization\" ;\n" \ + " rdfs:comment \"By default, all non-Admin agents (foaf:Agent) only " \ + " have read access (acl:Read) to the repository\" ;\n" \ + " acl:agentClass foaf:Agent ;\n" \ + " acl:mode acl:Read ;\n" \ + " acl:accessTo ;\n" \ + " acl:default .\n" \ + "\n" \ + "<{0}/fcr:tx#authz_read_write> a acl:Authorization ;\n" \ + " rdfs:label \"Test Tx Authorization\" ;\n" \ + " rdfs:comment \"Provide read write access to the transaction endpoint\" ;\n" \ + " acl:agentClass acl:AuthenticatedAgent ;\n" \ + " acl:mode acl:Read, acl:Write ;\n" \ + " acl:accessTo ;\n" \ + " acl:default .\n".format(fedora_base_uri) + + r = self.do_put(self.getFedoraBase() + "/" + TC.FCR_ACL, {'Content-type': 'text/turtle'}, root_acl) + self.checkResponse([TC.CREATED, TC.NO_CONTENT], r) + self.log("Create a resource") r = self.do_post() self.checkResponse(TC.CREATED, r) @@ -282,10 +457,12 @@ def aPlainUserTransactionRollback(self): " acl:mode acl:Read, acl:Write, acl:Append, acl:Control ;\n" \ " acl:accessTo <{2}> ;\n" \ " acl:default <{2}> ;\n" \ - " acl:agent \"{3}\" .\n".format(TC.ACL_NS, TC.FEDORA_NS, container_id, self.config[TC.USER_NAME_PARAM]) + " acl:agent \"{3}\" .\n".format(TC.ACL_NS, TC.FEDORA_NS, container_id, + self.config[TC.USER_NAME_PARAM]) self.log("Set up ACL with user having full access.") - r = self.do_put(container_id + "/" + TC.FCR_ACL, headers={"Content-type": "text/turtle"}, body=auth, admin=True) + r = self.do_put(container_id + "/" + TC.FCR_ACL, headers={"Content-type": "text/turtle"}, body=auth, + admin=True) self.checkResponse(TC.CREATED, r) self.log("Have user read the item") @@ -293,7 +470,8 @@ def aPlainUserTransactionRollback(self): self.checkResponse(TC.OK, r) self.log("Have the user patch the item") patch = "INSERT DATA { <> \"Some title\" }" - r = self.do_patch(container_id, headers={"Content-type": "application/sparql-update"}, body=patch, admin=False) + r = self.do_patch(container_id, headers={"Content-type": "application/sparql-update"}, body=patch, + admin=False) self.checkResponse(TC.NO_CONTENT, r) self.log("Start a transaction") tx_id = self.createTransaction(False) @@ -302,9 +480,8 @@ def aPlainUserTransactionRollback(self): r = self.do_post(container_id, headers=tx_headers, admin=False) self.checkResponse(TC.CREATED, r, tx_id) child = self.get_location(r) - self.log("Test getting the child (" + child + ") in transaction") + self.log(f"Test getting the child ({child}) in transaction") r = self.do_get(child, admin=False, headers=tx_headers) self.checkResponse(TC.OK, r, tx_id) - self.log("Rollback transaction") - r = self.do_delete(tx_id, admin=False) - self.checkResponse(TC.NO_CONTENT, r, tx_id) + self.log(f"Test getting the child ({child}) outside the transaction") + return child, tx_id From f592ad51f4cc21b3f7c89ba6d02d88eb180868b8 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 1 Dec 2023 12:34:18 -0600 Subject: [PATCH 16/20] update docs --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 1aec699..8cabc3a 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ If a configuration cannot be found or the `-n|--site_name` argument is not prese ### Isolate tests You can also choose to run only a subset of all tests using the `-t|--tests` argument. It accepts a comma separated list of the following values which indicate which tests to run. +* `archivalgroup` - Archival Group tests * `authz` - Authorization tests * `basic` - Basic interaction tests * `camel` - Camel toolbox tests (see [note](#camel-tests)) @@ -109,6 +110,14 @@ Both of these systems must be fed by the fcrepo-camel-toolbox for this testing. ## Tests implemented +### archivalgroup +1. Create an archivalgroup container +1. Create an archivalgroup member, delete it and it's tombstone +1. Create an archivalgroup member, delete it and PUT over the tombstone +1. Try to PUT over an archivalgroup member tombstone with a different interaction model +1. Create and delete an archivalgroup, delete it's tombstone. +1. Create and delete an archivalgroup, and PUT over the tombstone + ### authz 1. Create a container called **cover** 1. Patch it to a pcdm:Object From 6526020e5631ce74a300144176c9a27280fb3ef7 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Tue, 27 Feb 2024 08:14:10 -0600 Subject: [PATCH 17/20] Updates for 6.5.0 --- abstract_fedora_tests.py | 6 ++-- archival_group_tests.py | 39 ++++++++++++++++++------- basic_interaction_tests.py | 41 +++++++++++++++++++++------ version_tests.py | 58 ++++++++++++++++++++++++++++++++++---- 4 files changed, 118 insertions(+), 26 deletions(-) diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index 318b70e..becbe53 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -249,8 +249,10 @@ def check_for_retest(self, uri: str): "in the configuration.") quit() - def getHeader(self, uri: str, header_name: str): - r = self.do_head(uri) + def getHeader(self, uri: str, header_name: str, headers: dict = None): + if headers is None: + headers = {} + r = self.do_head(uri, headers=headers) self.assertTrue(TestConstants.OK, r) if header_name in r.headers: return r.headers[header_name] diff --git a/archival_group_tests.py b/archival_group_tests.py index 6243b82..4b6c888 100644 --- a/archival_group_tests.py +++ b/archival_group_tests.py @@ -170,21 +170,40 @@ def testCreateAndPutOverArchivalGroup(self): r = self.do_put(container_location) self.checkResponse(TC.GONE, r) - self.log("Try to PUT a normal RDFResource over archivalgroup, with header") + self.log("Try to PUT a new ArchivalGroupt over archivalgroup, with header") r = self.do_put(container_location, headers={ + "Link": self.make_type(TC.ARCHIVAL_GROUP), TC.OVERWRITE_TOMBSTONE_HEADER: "true" }) - # TODO: This should not be a SERVER ERROR, but it could be a CONFLICT - self.checkResponse(TC.SERVER_ERROR, r) + self.checkResponse(TC.CREATED, r) - # TODO: If the above succeeds then this should all be removed. - self.log("Try to PUT an archivalgroup over the archivalgroup, with header") - r = self.do_put(container_location, headers={ - TC.OVERWRITE_TOMBSTONE_HEADER: "true", + @Test + def testCreateAndPutOverArchivalGroupWithDifferentType(self): + self.log("Create Archival Group container") + r = self.do_post(self.getBaseUri(), headers={ "Link": self.make_type(TC.ARCHIVAL_GROUP) }) + container_location = self.get_location(r) self.checkResponse(TC.CREATED, r) - r = self.do_get(container_location) + self.log("Create archivalgroup member") + r = self.do_post(container_location) + self.checkResponse(TC.CREATED, r) + member_location = self.get_location(r) + r = self.do_get(member_location) self.checkResponse(TC.OK, r) - headers = self.get_link_headers(r) - self.assertNotIn(TC.ARCHIVAL_GROUP, headers['type']) + + self.log("Try to delete archivalgroup") + r = self.do_delete(container_location) + self.checkResponse(TC.NO_CONTENT, r) + r = self.do_get(container_location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT over archivalgroup, without header") + r = self.do_put(container_location) + self.checkResponse(TC.GONE, r) + + self.log("Try to PUT a normal RDFResource over archivalgroup, with header") + r = self.do_put(container_location, headers={ + TC.OVERWRITE_TOMBSTONE_HEADER: "true" + }) + self.checkResponse(TC.CONFLICT, r) diff --git a/basic_interaction_tests.py b/basic_interaction_tests.py index af16da7..558d120 100644 --- a/basic_interaction_tests.py +++ b/basic_interaction_tests.py @@ -1,4 +1,5 @@ #!/bin/env python +import datetime import shutil import tempfile import time @@ -39,11 +40,11 @@ def createTestResource(self, type, files=None): def getEtag(self, uri): """ Get the eTag header for a specified URI. """ - return self.getHeader(uri, "ETag") + return self.getHeader(uri, "ETag").strip() def getStateToken(self, uri): """ Get the X-State-Token header for a specified URI. """ - return self.getHeader(uri, "X-State-Token") + return self.getHeader(uri, "X-State-Token").strip() def duplicateImage(self): new_file = os.path.join(tempfile.gettempdir(), 'temp_image.jpeg') @@ -344,14 +345,19 @@ def testChecksum(self): parent_uri = self.get_location(r) first_etag = self.getEtag(parent_uri) - self.log(f"First etag of parent is {first_etag}") - + first_state_token = self.getStateToken(parent_uri) + self.log(f"First eTag of parent is {first_etag} and state token is {first_state_token}") + time.sleep(0.5) self.log("Add child resource to parent") r = self.do_post(parent_uri) self.checkResponse(TC.CREATED, r) second_etag = self.getEtag(parent_uri) - self.log(f"Second etag of parent is {second_etag}") + second_state_token = self.getStateToken(parent_uri) + self.log(f"Second eTag of parent is {second_etag} and state token is {second_state_token}") + + self.assertNotEqual(first_etag, second_etag, "First and second state etags should not match") + self.assertEqual(first_state_token, second_state_token, "First and second state tokens should match") self.log("Add 30 child resources to parent") for i in range(1, 30): @@ -359,12 +365,31 @@ def testChecksum(self): self.checkResponse(TC.CREATED, r) third_etag = self.getEtag(parent_uri) - self.log(f"Third etag of parent is {third_etag}") - - self.assertNotEqual(first_etag, second_etag, "First and second state etags should not match") + third_state_token = self.getStateToken(parent_uri) + if third_etag == second_etag: + start = time.perf_counter() + self.log("Waiting up to 2 seconds to account for H2 delay on eTags") + while second_etag == third_etag and time.perf_counter() - start < 2: + time.sleep(.3) + third_etag = self.getEtag(parent_uri) + third_state_token = self.getStateToken(parent_uri) + self.log(f"Third eTag of parent is {third_etag} and state token is {third_state_token}") self.assertNotEqual(first_etag, third_etag, "First and third state etag should not match") self.assertNotEqual(second_etag, third_etag, "Second and third state etag should not match") + self.assertEqual(first_state_token, third_state_token, "First and third state tokens should match") + self.assertEqual(second_state_token, third_state_token, "Second and third state tokens should match") + + r = self.do_patch(parent_uri, headers={'Content-type': TC.SPARQL_UPDATE_MIMETYPE}, + body="INSERT {<> <" + TC.DC_TITLE + "> 'Some title'. } WHERE {}") + self.checkResponse(TC.NO_CONTENT, r) + + fourth_etag = self.getEtag(parent_uri) + fourth_state_token = self.getStateToken(parent_uri) + self.log(f"Fourth eTag of parent is {fourth_etag} and state token is {fourth_state_token}") + self.assertNotEqual(third_etag, fourth_etag, "Third and fourth state etag should match") + self.assertNotEqual(third_state_token, fourth_state_token, "Third and fourth state tokens should not match") + # @Test # These tests require Fedora to allow the paths, might require more thought. def testExternalContentProxyLocal(self): self.log("Create external content to local resource") diff --git a/version_tests.py b/version_tests.py index 07e2042..6a06a4f 100644 --- a/version_tests.py +++ b/version_tests.py @@ -1,4 +1,7 @@ #!/bin/env python +import json + +import pyjq import TestConstants from abstract_fedora_tests import FedoraTests, register_tests, Test @@ -29,16 +32,27 @@ def get_all_mementos(response): def count_mementos(response): return len(FedoraVersionTests.get_all_mementos(response)) - def checkMementoCount(self, expected, uri, admin=None): + def checkMementoCount(self, expected, uri, admin=None, use_link_format=True): if admin is None: admin = True uri = get_version_endpoint(uri) + if use_link_format: + rdf_type = TestConstants.LINK_FORMAT_MIMETYPE + else: + rdf_type = TestConstants.JSONLD_MIMETYPE headers = { - 'Accept': TestConstants.LINK_FORMAT_MIMETYPE + 'Accept': rdf_type } r = self.do_get(uri, headers=headers, admin=admin) self.checkResponse(200, r) - self.checkValue(expected, self.count_mementos(r)) + if rdf_type == TestConstants.LINK_FORMAT_MIMETYPE: + self.checkValue(expected, self.count_mementos(r)) + else: + body = r.content.decode('UTF-8') + json_body = json.loads(body) + found_title = pyjq.all('.[] | ."@type" | .[]', json_body) + matching = [x for x in found_title if x == expected] + self.checkValue(expected, len(matching)) def getNthMemento(self, uri, memento_number=1, admin=None): if admin is None: @@ -134,6 +148,35 @@ def doContainerVersioningTest(self): r = self.do_get(memento_location) self.checkResponse(TestConstants.OK, r) + @Test + def makeVersionsInsideASecond(self): + headers = { + 'Link': self.make_type(TestConstants.LDP_BASIC) + } + r = self.do_post(self.getBaseUri(), headers=headers) + self.log("Create a basic container") + self.checkResponse(TestConstants.CREATED, r) + location = self.get_location(r) + self.log("at " + location) + + version_endpoint = location + "/" + TestConstants.FCR_VERSIONS + r = self.do_get(version_endpoint) + self.checkResponse(TestConstants.OK, r) + + self.log("Create a version") + r = self.do_post(version_endpoint) + self.checkResponse(TestConstants.CREATED, r) + memento_location = self.get_location(r) + + self.log("Create another version") + r = self.do_post(version_endpoint) + self.checkResponse(TestConstants.CREATED, r) + + self.log("Count mementos") + # Only one memento as they are both in the same second. + + self.checkMementoCount(1, version_endpoint) + @Test def doBinaryVersioningTest(self): headers = { @@ -352,7 +395,7 @@ def createBinaryVersionsAtSameTime(self): self.checkMementoCount(1, description_version_endpoint) @Test - def testMementoAreInaccessibleAfterDelete(self): + def testMementoAreAccessibleAfterDelete(self): r = self.do_post() self.checkResponse(TestConstants.CREATED, r) uri = self.get_location(r) @@ -362,6 +405,8 @@ def testMementoAreInaccessibleAfterDelete(self): r = self.do_post(uri + "/" + TestConstants.FCR_VERSIONS) self.checkResponse(TestConstants.CREATED, r) memento = self.get_location(r) + self.log("Wait a second or the memento and the delete will occur in the same second") + time.sleep(1) self.verifyGet(memento) @@ -369,8 +414,9 @@ def testMementoAreInaccessibleAfterDelete(self): self.checkResponse(TestConstants.NO_CONTENT, r) self.verifyGone(uri) - self.verifyGone(uri + "/" + TestConstants.FCR_VERSIONS) - self.verifyGone(memento) + # TimeMaps and Mementos are accessible after the resource is deleted (but not purged) as of 6.5.0 + self.verifyGet(uri + "/" + TestConstants.FCR_VERSIONS) + self.verifyGet(memento) @Test def testBinaryDescription(self): From fc691646bf38da052e8c562919bbfb2f67cd72a5 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Thu, 23 May 2024 16:03:26 -0500 Subject: [PATCH 18/20] Fix SMT using ldp:contains test with Direct/Indirect Container Make versions at the same time --- indirect_tests.py | 33 +++++++++++++++++++++++++++++++++ version_tests.py | 25 +++++++++++++++---------- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/indirect_tests.py b/indirect_tests.py index 1a7c6d2..462d1c2 100644 --- a/indirect_tests.py +++ b/indirect_tests.py @@ -196,3 +196,36 @@ def doAddIndirect(self): self.assertEqual(403, r.status_code, "Did not get expected status code") self.log("Indirect is {0}\nread-only is {1}".format(indirect_location, read_only_location)) + + @Test + def testDirectWithServerManaged(self): + self.log("Try to create a direct container with server managed ldp:hasMemberRelation") + headers = { + 'Link': self.make_type(TestConstants.LDP_INDIRECT), + 'Content-type': 'text/turtle' + } + indirect_body = "@prefix ldp: .\n" \ + "@prefix dc: .\n" \ + "<> ldp:hasMemberRelation ldp:contains ;\n" \ + "dc:title \"Members Container\" ." + r = self.do_post(self.getBaseUri(), headers=headers, body=indirect_body) + self.assertEqual(409, r.status_code, "Did not get expected status code") + + @Test + def testIndirectWithServerManaged(self): + self.log("Try to create an indirect container with server managed ldp:hasMemberRelation") + r = self.do_post() + # Create a container + self.checkResponse(TestConstants.CREATED, r) + location = self.get_location(r) + headers = { + 'Link': self.make_type(TestConstants.LDP_INDIRECT), + 'Content-type': 'text/turtle' + } + indirect_body = "@prefix ldp: .\n" \ + "@prefix dc: .\n" \ + "<> ldp:insertedContentRelation <{0}> ;\n" \ + "ldp:hasMemberRelation ldp:contains ;\n" \ + "dc:title \"Members Container\" .".format(location) + r = self.do_post(self.getBaseUri(), headers=headers, body=indirect_body) + self.assertEqual(409, r.status_code, "Did not get expected status code") diff --git a/version_tests.py b/version_tests.py index 6a06a4f..b3d75c9 100644 --- a/version_tests.py +++ b/version_tests.py @@ -1,4 +1,5 @@ #!/bin/env python +import collections import json import pyjq @@ -8,6 +9,7 @@ import time import rdflib from rdflib.namespace import DC, RDF +import concurrent.futures as futures def get_version_endpoint(uri): @@ -162,20 +164,23 @@ def makeVersionsInsideASecond(self): version_endpoint = location + "/" + TestConstants.FCR_VERSIONS r = self.do_get(version_endpoint) self.checkResponse(TestConstants.OK, r) + self.checkMementoCount(1, version_endpoint) - self.log("Create a version") - r = self.do_post(version_endpoint) - self.checkResponse(TestConstants.CREATED, r) - memento_location = self.get_location(r) + time.sleep(1) - self.log("Create another version") - r = self.do_post(version_endpoint) - self.checkResponse(TestConstants.CREATED, r) + self.log("Create multiple versions inside a second") + with futures.ThreadPoolExecutor(max_workers=3) as ec: + results = ec.map(self.do_post, [version_endpoint, version_endpoint, version_endpoint]) + status_codes = [r.status_code for r in results] + counter = collections.Counter(status_codes) + self.assertEqual(2, counter[TestConstants.CONFLICT]) + self.assertEqual(1, counter[TestConstants.CREATED]) self.log("Count mementos") - # Only one memento as they are both in the same second. - - self.checkMementoCount(1, version_endpoint) + # Only one new memento as they are both in the same second. + self.checkMementoCount(2, version_endpoint) + # But multiple actual versions created + self.checkMementoCount(2, version_endpoint, use_link_format=True) @Test def doBinaryVersioningTest(self): From 31f7b27b0692b92d010fe3b2060764a14a5a6cc4 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Fri, 24 May 2024 10:40:50 -0500 Subject: [PATCH 19/20] Add comments to tests --- README.md | 26 ++++++---- TestConstants.py | 28 +++++----- abstract_fedora_tests.py | 4 ++ archival_group_tests.py | 8 +++ authz_tests.py | 18 +++++-- basic_interaction_tests.py | 22 +++++++- camel_tests.py | 1 + fixity_tests.py | 6 +-- indirect_tests.py | 104 +++++++++++++++++++++---------------- rdf_tests.py | 40 +++++++------- sparql_tests.py | 14 +++++ ssearch_tests.py | 2 + transaction_tests.py | 32 +++++------- version_tests.py | 13 +++-- 14 files changed, 197 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 8cabc3a..bb9d68a 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,12 @@ -# Fedora 4 Tests +# Fedora Tests -These python tests are meant to be run against a standalone Fedora 4 instance. +These python tests are meant to be run against a standalone Fedora instance. This will create, update and delete resources in the repository. So you may **not** want to use it on a production instance. Also, this is doing a cursory test. It does some verification of RDF, and patches are always welcome. -Note: in order to test authorization, please first verify that your Fedora repository in configured to use authorization. -Check the `repository.json` in use, and verify that the `security` block contains a `providers` list such as: - - "providers" : [ - { "classname" : "org.fcrepo.auth.common.ServletContainerAuthenticationProvider" } - ] +Note: in order to test authorization, please see [this note](#authz-tests) ## Installation @@ -83,11 +78,11 @@ If a configuration cannot be found or the `-n|--site_name` argument is not prese You can also choose to run only a subset of all tests using the `-t|--tests` argument. It accepts a comma separated list of the following values which indicate which tests to run. * `archivalgroup` - Archival Group tests -* `authz` - Authorization tests +* `authz` - Authorization tests (see [note](#authz-tests)) * `basic` - Basic interaction tests * `camel` - Camel toolbox tests (see [note](#camel-tests)) * `fixity` - Binary fixity tests -* `indirect` - Indirect container tests +* `indirect` - Direct/Indirect container tests * `rdf` - RDF serialization tests * `sparql` - Sparql operation tests * `transaction` - Transcation tests @@ -108,8 +103,19 @@ They also require the configuration to have a `solrurl` parameter pointing to a Both of these systems must be fed by the fcrepo-camel-toolbox for this testing. +##### AuthZ tests +`authz` tests will require authz to be enabled on your Fedora, as well you will need to set the +following properties for your Fedora or the test `testGroupAuth` will fail. +```commandline +fcrepo.auth.webac.userAgent.baseUri=http://example.com/ +fcrepo.auth.webac.groupAgent.baseUri=http://example.com/ +``` + + ## Tests implemented +**Note**: this list is out of date, you are better to view the tests in the various test classes. + ### archivalgroup 1. Create an archivalgroup container 1. Create an archivalgroup member, delete it and it's tombstone diff --git a/TestConstants.py b/TestConstants.py index 5e4ceb6..28c43d7 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -71,20 +71,28 @@ ACL_NS = "http://www.w3.org/ns/auth/acl#" -PURL_NS = "http://purl.org/dc/elements/1.1/" +DC_NS = "http://purl.org/dc/elements/1.1/" + +RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + +PCDM_NS = "http://pcdm.org/models#" + +# Some standard properties +DC_TITLE = "{}title".format(DC_NS) +RDF_TYPE = "{}type".format(RDF_NS) # Test constructs -OBJECT_TTL = "@prefix dc: ." \ - "@prefix pcdm: ." \ +OBJECT_TTL = "@prefix dc: <{0}> ." \ + "@prefix pcdm: <{1}> ." \ "<> a pcdm:Object ;" \ - "dc:title \"An Object\" ." + "dc:title \"An Object\" .".format(DC_NS, PCDM_NS) PCDM_CONTAINER_TITLE = "PCDM Container" -PCDM_CONTAINER_TTL = "@prefix dc: ." \ - "@prefix pcdm: ." \ +PCDM_CONTAINER_TTL = "@prefix dc: <{1}> ." \ + "@prefix pcdm: <{2}> ." \ "<> a pcdm:Object ;" \ - "dc:title \"{0}\" .".format(PCDM_CONTAINER_TITLE) + "dc:title \"{0}\" .".format(PCDM_CONTAINER_TITLE, DC_NS, PCDM_NS) # Standard Response codes CREATED = 201 NO_CONTENT = 204 @@ -98,8 +106,4 @@ METHOD_NOT_ALLOWED = 405 SERVER_ERROR = 500 -# Some standard properties -DC_NAMESPACE = "http://purl.org/dc/elements/1.1/" -RDF_NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" -DC_TITLE = "{}title".format(DC_NAMESPACE) -RDF_TYPE = "{}type".format(RDF_NAMESPACE) + diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index becbe53..9234403 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -25,6 +25,9 @@ class FedoraTests(unittest.TestCase): # Holds ultimate success or failure for tests. results = {} + """ Holds the container resource which test objects are placed in """ + CONTAINER = "" + def __init__(self, config): super().__init__() if 'debug_level' not in config: @@ -191,6 +194,7 @@ def assert_regex_matches(self, pattern, text, msg): """ Do a regex match against a string """ if re.search(pattern, text): return + standard_msg = '%s pattern not matched in %s' % (unittest.util.safe_repr(pattern), unittest.util.safe_repr(text)) self.fail(self._formatMessage(msg, standard_msg)) diff --git a/archival_group_tests.py b/archival_group_tests.py index 4b6c888..ec8b130 100644 --- a/archival_group_tests.py +++ b/archival_group_tests.py @@ -11,6 +11,7 @@ class FedoraArchivalGroupTests(FedoraTests): @Test def testCreateArchivalGroup(self): + """ Test create an archival group """ self.log("Create Archival Group container") r = self.do_post(self.getBaseUri(), headers={ "Link": self.make_type(TC.ARCHIVAL_GROUP) @@ -23,6 +24,7 @@ def testCreateArchivalGroup(self): @Test def testDeleteArchivalGroupMember(self): + """ Test creating a member of an archival group, deleting it and trying to purge it. """ self.log("Create Archival Group container") r = self.do_post(self.getBaseUri(), headers={ "Link": self.make_type(TC.ARCHIVAL_GROUP) @@ -48,6 +50,7 @@ def testDeleteArchivalGroupMember(self): @Test def putOverArchivalGroupMember(self): + """ Test creating an archival group member, delete the member and then PUT overtop """ self.log("Create Archival Group container") r = self.do_post(self.getBaseUri(), headers={ "Link": self.make_type(TC.ARCHIVAL_GROUP) @@ -79,6 +82,7 @@ def putOverArchivalGroupMember(self): @Test def putOverArchivalGroupMemberWithDifferentType(self): + """ Try to PUT a resource over an archival group member with a different interaction model """ self.log("Create Archival Group container") r = self.do_post(self.getBaseUri(), headers={ "Link": self.make_type(TC.ARCHIVAL_GROUP) @@ -115,6 +119,7 @@ def putOverArchivalGroupMemberWithDifferentType(self): @Test def testCreateAndDeleteArchivalGroup(self): + """ Test creating and deleting an archival group """ self.log("Create Archival Group container") r = self.do_post(self.getBaseUri(), headers={ "Link": self.make_type(TC.ARCHIVAL_GROUP) @@ -147,6 +152,7 @@ def testCreateAndDeleteArchivalGroup(self): @Test def testCreateAndPutOverArchivalGroup(self): + """ Test create and delete an archival group and put overtop the tombstone """ self.log("Create Archival Group container") r = self.do_post(self.getBaseUri(), headers={ "Link": self.make_type(TC.ARCHIVAL_GROUP) @@ -179,6 +185,8 @@ def testCreateAndPutOverArchivalGroup(self): @Test def testCreateAndPutOverArchivalGroupWithDifferentType(self): + """ Test create/delete an archival group and try to PUT overtop the tombstone with a different interaction + model """ self.log("Create Archival Group container") r = self.do_post(self.getBaseUri(), headers={ "Link": self.make_type(TC.ARCHIVAL_GROUP) diff --git a/authz_tests.py b/authz_tests.py index a0ae6c1..569c78f 100644 --- a/authz_tests.py +++ b/authz_tests.py @@ -47,6 +47,7 @@ def getAclUri(response): @Test def doAuthTests(self): + """ Basic permissions test """ self.verifyAuthEnabled() self.log("Create \"cover\" container") @@ -142,6 +143,7 @@ def doAuthTests(self): @Test def doDirectIndirectAuthTests(self): + """ Test that direct and indirect containers require permissions to the ldp:membershipResource too """ self.verifyAuthEnabled() self.log("Create a target container") @@ -250,6 +252,7 @@ def doDirectIndirectAuthTests(self): @Test def multipleAuthzCreatePermissiveSet(self): + """ Ensure that multiple ACLs result in the most permissive union of all the ACLs """ self.verifyAuthEnabled() self.log("Create a target container") @@ -284,14 +287,14 @@ def multipleAuthzCreatePermissiveSet(self): headers = { 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } - body = "prefix dc: <{0}> INSERT {{ <> dc:title \"A new title\" }} WHERE {{}}".format(TC.PURL_NS) + body = "prefix dc: <{0}> INSERT {{ <> dc:title \"A new title\" }} WHERE {{}}".format(TC.DC_NS) r = self.do_patch(target_location, headers=headers, body=body) self.checkResponse(204, r) @Test def testAllThingsPointTogether(self): self.verifyAuthEnabled() - + """ Ensure we show the same ACL location for binaries and their metadata """ self.log("Create a target binary") headers = { 'Content-type': 'text/plain', @@ -326,7 +329,6 @@ def testAllThingsPointTogether(self): self.log("Create a version of binary") r = self.do_post(parent=binary_versions) self.checkResponse(201, r) - memento_location = self.get_location(r) self.log("Check binary description timemap's acl link header is correct.") binary_metadata_versions = binary_description + "/fcr:versions" @@ -337,6 +339,7 @@ def testAllThingsPointTogether(self): @Test def doBinaryAndMetadataShareACL(self): + """ Test that a binary and it's metadata share the same ACL permissions """ self.verifyAuthEnabled() self.log("Create a target binary") @@ -379,7 +382,7 @@ def doBinaryAndMetadataShareACL(self): self.checkResponse(403, r) self.log("Try to patch metadata") - patch_body = "prefix dc: <{0}> INSERT DATA {{ <> dc:title \"Updated title\"}}".format(TC.PURL_NS) + patch_body = "prefix dc: <{0}> INSERT DATA {{ <> dc:title \"Updated title\"}}".format(TC.DC_NS) headers = { 'Content-type': TC.SPARQL_UPDATE_MIMETYPE } @@ -388,7 +391,7 @@ def doBinaryAndMetadataShareACL(self): @Test def testContainerWithAccessToClass(self): - + """ Test permissions using AccessToClass """ self.verifyAuthEnabled() self.log("Create a container") @@ -461,6 +464,7 @@ def testContainerWithAccessToClass(self): @Test def testPermissionsDoNotExtendInTx(self): + """ Ensure permissions work in transactions and do not extend """ self.verifyAuthEnabled() self.log("Create a container") r = self.do_post() @@ -521,6 +525,7 @@ def testPermissionsDoNotExtendInTx(self): @Test def testGroupAuth(self): + """ Test authentication using a group """ self.log("THIS TEST REQUIRES FEDORA TO HAVE THE TESTSUITE WEBID PREFIX") agentGroup = "@prefix acl: . " \ "@prefix vcard: . " \ @@ -553,6 +558,7 @@ def testGroupAuth(self): @Test def testControlOnlyPut(self): + """ Test that a user with Control permission can only PUT to the ACL and not read it. """ r = self.do_post() self.checkResponse(201, r) resource_uri = self.get_location(r) @@ -590,6 +596,7 @@ def testControlOnlyPut(self): @Test def testCanAccessToAndAccessToClass(self): + """ Test you can't use both acl:accessTo and acl:accessToClass on the same ACL """ r = self.do_post() self.checkResponse(201, r) resource_uri = self.get_location(r) @@ -613,6 +620,7 @@ def testCanAccessToAndAccessToClass(self): @Test def testPutInvalidAcl(self): + """ Test you can't define the ACL location """ self.log("Create a mock container to use as the ACL") r = self.do_post() self.checkResponse(201, r) diff --git a/basic_interaction_tests.py b/basic_interaction_tests.py index 558d120..b674c18 100644 --- a/basic_interaction_tests.py +++ b/basic_interaction_tests.py @@ -1,5 +1,4 @@ #!/bin/env python -import datetime import shutil import tempfile import time @@ -47,18 +46,21 @@ def getStateToken(self, uri): return self.getHeader(uri, "X-State-Token").strip() def duplicateImage(self): + """ Copy the test image to a location """ new_file = os.path.join(tempfile.gettempdir(), 'temp_image.jpeg') shutil.copyfile(self.getImagePath(), new_file) return new_file @Test - def aTestMissingResource(self): + def testMissingResource(self): + """ Test we get a 404 for a non-existant resource """ fake_id = str(uuid.uuid4()) r = self.do_get(self.getFedoraBase() + "/" + fake_id) self.assertEqual(404, r.status_code, "Did not get expected response") @Test def testDeleteAResource(self): + """ Test that we can reuse an atomic resource URL once it is purged """ self.log("Create container") r = self.do_post(self.getBaseUri()) container_location = self.get_location(r) @@ -100,18 +102,22 @@ def testDeleteAResource(self): @Test def testBasicContainer(self): + """ Test creating a basic container """ self.createTestResource(TC.LDP_BASIC) @Test def testDirectContainer(self): + """ Test creating a direct container """ self.createTestResource(TC.LDP_DIRECT) @Test def testIndirectContainer(self): + """ Test creating an indirect container """ self.createTestResource(TC.LDP_INDIRECT) @Test def testNonRdfSource(self): + """ Test creating a binary """ testfiles = {'files': ('testdata.csv', 'this,is,some,data\n')} self.createTestResource(TC.LDP_NON_RDF_SOURCE, files=testfiles) @@ -137,6 +143,7 @@ def testLdpContainer(self): @Test def doNestedTests(self): + """ Test creating child objects and removing them """ self.log("Create a container") r = self.createBasicContainer(self.getBaseUri()) self.checkResponse(TC.CREATED, r) @@ -206,6 +213,7 @@ def doNestedTests(self): @Test def testPurgeContainer(self): + """ Test create, delete and purge a container """ r = self.do_post() self.checkResponse(TC.CREATED, r) uri = self.get_location(r) @@ -233,6 +241,7 @@ def testPurgeContainer(self): @Test def testPurgeBinary(self): + """ Test create, delete and purge a binary """ headers = { 'Link': "<{}>; rel=\"type\"".format(TC.LDP_NON_RDF_SOURCE) } @@ -311,6 +320,7 @@ def changeIxnModels(self, location, starting_model): @Test def testChangeIxnModel(self): + """ Test responses when trying to change interaction models """ self.log("Create a basic container") basic = self.createTestResource(TC.LDP_BASIC) self.changeIxnModels(basic, TC.LDP_BASIC) @@ -328,7 +338,9 @@ def testChangeIxnModel(self): non_rdf = self.createTestResource(TC.LDP_NON_RDF_SOURCE, files=testfiles) self.changeIxnModels(non_rdf, TC.LDP_NON_RDF_SOURCE) + @Test def testBinaryTriples(self): + """ Test you can create a binary with some expected headers """ self.log("Create binary with expected properties") headers = { 'Content-type': 'text/plain', @@ -339,6 +351,8 @@ def testBinaryTriples(self): @Test def testChecksum(self): + """ Test that interaction of state tokens and eTags + TODO: This test flaps a bit """ self.log("Create parent resource") r = self.do_post(self.getBaseUri()) self.checkResponse(TC.CREATED, r) @@ -452,6 +466,7 @@ def testExternalContentHttpProxy(self): @Test def testDeleteAndPutOverTombstoneRdf(self): + """ Test you need the header for PUT over a RDFSource tombstone """ self.log("Create resource") r = self.do_post() self.checkResponse(TC.CREATED, r) @@ -475,6 +490,7 @@ def testDeleteAndPutOverTombstoneRdf(self): @Test def testDeleteAndPutOverTombstoneNonRdf(self): + """ Test you need the header for PUT over a NonRDFSource tombstone """ self.log("Create NonRdf resource") r = self.do_post(headers={ 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE), @@ -506,6 +522,7 @@ def testDeleteAndPutOverTombstoneNonRdf(self): @Test def testDeleteAndPutOverTombstoneWrongTypeRdf(self): + """ Test you can't PUT a NonRDFSource over a tombstone for a RDFSource """ self.log("Create resource RDF") r = self.do_post() self.checkResponse(TC.CREATED, r) @@ -534,6 +551,7 @@ def testDeleteAndPutOverTombstoneWrongTypeRdf(self): @Test def testDeleteAndPutOverTombstoneWrongTypeNonRdf(self): + """ Test you can't PUT a RDFSource over a tombstone for a NonRDFSource """ self.log("Create resource NonRDF") r = self.do_post(headers={ 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE), diff --git a/camel_tests.py b/camel_tests.py index d85716b..fe27616 100644 --- a/camel_tests.py +++ b/camel_tests.py @@ -26,6 +26,7 @@ def run_tests(self): @Test def CamelCreateObject(self): + """ Test create an object in Fedora and track it into Solr/Triplestore """ self.log("Create an object") internal_id = str(uuid.uuid4()) expected_url = self.getBaseUri() + "/" + internal_id diff --git a/fixity_tests.py b/fixity_tests.py index 9058d64..f54532b 100644 --- a/fixity_tests.py +++ b/fixity_tests.py @@ -1,10 +1,6 @@ #!/bin/env python -import TestConstants from abstract_fedora_tests import FedoraTests, register_tests, Test -import os.path -import pyjq -import json @register_tests @@ -25,7 +21,7 @@ def decode_digest_header(self, header): @Test def aFixityTest(self): - + """ Test doing a fixity test and that it matches the expected result. """ self.log("Create a binary") headers = { 'Content-type': 'image/jpeg', diff --git a/indirect_tests.py b/indirect_tests.py index 462d1c2..f829b5c 100644 --- a/indirect_tests.py +++ b/indirect_tests.py @@ -9,12 +9,12 @@ @register_tests class FedoraIndirectTests(FedoraTests): - # Create test objects all inside here for ease of review/removal CONTAINER = "/test_indirect" @Test def doPcdmIndirect(self): + """ Test that we can create a working indirect container """ self.log("Create a PCDM container") basic_headers = { 'Link': self.make_type(TestConstants.LDP_BASIC), @@ -34,23 +34,28 @@ def doPcdmIndirect(self): 'Link': self.make_type(TestConstants.LDP_INDIRECT), 'Content-type': 'text/turtle' } - pcdm_indirect = "@prefix dc: ." \ - "@prefix pcdm: ." \ + pcdm_indirect = "@prefix dc: <{1}> ." \ + "@prefix pcdm: <{2}> ." \ "@prefix ore: ." \ - "@prefix ldp: ." \ + "@prefix ldp: <{3}> ." \ "<> dc:title \"Members Container\" ;" \ " ldp:membershipResource <{0}> ;" \ " ldp:hasMemberRelation pcdm:hasMember ;" \ - " ldp:insertedContentRelation ore:proxyFor .".format(pcdm_collection_location) + " ldp:insertedContentRelation ore:proxyFor .".format( + pcdm_collection_location, + TestConstants.DC_NS, + TestConstants.PCDM_NS, + TestConstants.LDP_NS + ) r = self.do_post(pcdm_collection_location, headers=indirect_headers, body=pcdm_indirect) self.assertEqual(201, r.status_code, "Did not get expected status code") members_location = self.get_location(r) self.log("Create a proxy object") - pcdm_proxy = "@prefix pcdm: " \ + pcdm_proxy = "@prefix pcdm: <{1}>" \ "@prefix ore: " \ "<> a pcdm:Object ;" \ - "ore:proxyFor <{0}> .".format(pcdm_container_location) + "ore:proxyFor <{0}> .".format(pcdm_container_location, TestConstants.PCDM_NS) r = self.do_post(members_location, headers=basic_headers, body=pcdm_proxy) self.assertEqual(201, r.status_code, "Did not get expected status code") @@ -72,6 +77,7 @@ def doPcdmIndirect(self): @Test def doAddIndirect(self): + """ Test indirect container interactions with a resource you don't have access to """ read_only = str(uuid.uuid4()) headers = { 'Slug': read_only @@ -84,11 +90,15 @@ def doAddIndirect(self): turtle_headers = { 'Content-type': TestConstants.TURTLE_MIMETYPE } - read_only_acl = "@prefix acl: . \n"\ - "<#readauthz> a acl:Authorization ; \n"\ - "acl:agent \"{0}\" ;\n" \ - "acl:mode acl:Read ;\n" \ - "acl:accessTo <{1}> .".format(self.config[TestConstants.USER_NAME_PARAM], read_only_location) + read_only_acl = "@prefix acl: <{2}> . \n" \ + "<#readauthz> a acl:Authorization ; \n" \ + "acl:agent \"{0}\" ;\n" \ + "acl:mode acl:Read ;\n" \ + "acl:accessTo <{1}> .".format( + self.config[TestConstants.USER_NAME_PARAM], + read_only_location, + TestConstants.ACL_NS + ) self.log("adding an acl to the read-only resource") r = self.do_put(read_only_location + "/" + TestConstants.FCR_ACL, headers=turtle_headers, body=read_only_acl) self.assertEqual(201, r.status_code, "Did not get expected status code") @@ -100,7 +110,7 @@ def doAddIndirect(self): patch_headers = { "Content-type": TestConstants.SPARQL_UPDATE_MIMETYPE } - patch_body = "INSERT DATA { <> \"Changed it\"}" + patch_body = "INSERT DATA { <> <" + TestConstants.DC_NS + "title> \"Changed it\"}" self.log("Try patch as testuser") r = self.do_patch(read_only_location, headers=patch_headers, body=patch_body, admin=False) self.assertEqual(403, r.status_code, "Did not get expected status code") @@ -114,12 +124,16 @@ def doAddIndirect(self): writeable_location = self.get_location(r) self.log("create a new writeable resource at {0}".format(writeable_location)) - writeable_acl = "@prefix acl: .\n" \ - "<#writeauth> a acl:Authorization ;\n" \ - " acl:agent \"{0}\" ;\n" \ - " acl:mode acl:Read, acl:Write ;\n" \ - " acl:accessTo <{1}> ;\n" \ - " acl:default <{1}> .".format(self.config[TestConstants.USER_NAME_PARAM], writeable_location) + writeable_acl = "@prefix acl: <{2}> .\n" \ + "<#writeauth> a acl:Authorization ;\n" \ + " acl:agent \"{0}\" ;\n" \ + " acl:mode acl:Read, acl:Write ;\n" \ + " acl:accessTo <{1}> ;\n" \ + " acl:default <{1}> .".format( + self.config[TestConstants.USER_NAME_PARAM], + writeable_location, + TestConstants.ACL_NS + ) self.log("create an ACl on the writeable resource") r = self.do_put(writeable_location + "/" + TestConstants.FCR_ACL, headers=turtle_headers, body=writeable_acl) self.assertEqual(201, r.status_code, "Did not get expected status code") @@ -128,14 +142,14 @@ def doAddIndirect(self): r = self.do_post(writeable_location, admin=False) self.assertEqual(201, r.status_code, "Did not get expected status code") - indirect_template = "@prefix ldp: .\n" \ - "@prefix example: .\n" \ - "@prefix dc: .\n" \ - "<> ldp:insertedContentRelation ;\n" \ - "ldp:membershipResource <{0}> ;\n" \ - "ldp:hasMemberRelation ;\n" \ - "dc:title \"The indirect container\" ." - indirect_body = indirect_template.format(read_only_location) + indirect_template = "@prefix ldp: <{1}> .\n" \ + "@prefix example: .\n" \ + "@prefix dc: <{2}> .\n" \ + "<> ldp:insertedContentRelation ;\n" \ + "ldp:membershipResource <{0}> ;\n" \ + "ldp:hasMemberRelation ;\n" \ + "dc:title \"The indirect container\" ." + indirect_body = indirect_template.format(read_only_location, TestConstants.LDP_NS, TestConstants.DC_NS) headers = { "Slug": "indirect", "Content-type": TestConstants.TURTLE_MIMETYPE, @@ -158,28 +172,28 @@ def doAddIndirect(self): "Content-type": TestConstants.TURTLE_MIMETYPE, 'Link': self.make_type(TestConstants.LDP_INDIRECT) } - mock_body = indirect_template.format(mockTarget_location) + mock_body = indirect_template.format(mockTarget_location, TestConstants.LDP_NS, TestConstants.DC_NS) self.log("Create an indirect container referencing an allowed target") r = self.do_post(writeable_location, headers=headers, body=mock_body, admin=False) self.assertEqual(201, r.status_code, 'Did not get expected status code') indirect_location = self.get_location(r) - insert_delete_patch = "prefix ldp: \n" \ - "DELETE {{ <> ldp:membershipResource ?o }} \n" \ - "INSERT {{ <> ldp:membershipResource <{0}> }} \n" \ - "WHERE {{ <> ldp:membershipResource ?o }}".format(read_only_location) + insert_delete_patch = "prefix ldp: <{1}> \n" \ + "DELETE {{ <> ldp:membershipResource ?o }} \n" \ + "INSERT {{ <> ldp:membershipResource <{0}> }} \n" \ + "WHERE {{ <> ldp:membershipResource ?o }}".format(read_only_location, TestConstants.LDP_NS) self.log("Try to patch the indirect from allowed to not-allowed") r = self.do_patch(indirect_location, headers=patch_headers, body=insert_delete_patch, admin=False) self.assertEqual(403, r.status_code, 'Did not get expected status code') - delete_data_body = "prefix ldp: \n" \ - "DELETE DATA {{ <> ldp:membershipResource <{0}> }}".format(mockTarget_location) + delete_data_body = "prefix ldp: <{1}> \n" \ + "DELETE DATA {{ <> ldp:membershipResource <{0}> }}".format(mockTarget_location, TestConstants.LDP_NS) self.log("Try to DELETE DATA the membershipResource") r = self.do_patch(indirect_location, headers=patch_headers, body=delete_data_body, admin=False) self.assertEqual(204, r.status_code, 'Did not get expected status code') - insert_data_body = "prefix ldp: \n" \ - "INSERT DATA {{ <> ldp:membershipResource <{0}> }}".format(read_only_location) + insert_data_body = "prefix ldp: <{1}> \n" \ + "INSERT DATA {{ <> ldp:membershipResource <{0}> }}".format(read_only_location, TestConstants.LDP_NS) self.log("Try to INSERT DATA the membershipResource") r = self.do_patch(indirect_location, headers=patch_headers, body=insert_data_body, admin=False) self.assertEqual(403, r.status_code, 'Did not get expected status code') @@ -189,9 +203,9 @@ def doAddIndirect(self): self.assertEqual(204, r.status_code, 'Did not get expected status code') self.log("Now try to post to the indirect") - post_body = "@prefix ldp: .\n" \ - "@prefix test: .\n\n" \ - "<> test:something <{0}> .".format(mockTarget_location) + post_body = "@prefix ldp: <{1}> .\n" \ + "@prefix test: .\n\n" \ + "<> test:something <{0}> .".format(mockTarget_location, TestConstants.LDP_NS) r = self.do_post(indirect_location, headers=turtle_headers, body=post_body, admin=False) self.assertEqual(403, r.status_code, "Did not get expected status code") @@ -204,10 +218,10 @@ def testDirectWithServerManaged(self): 'Link': self.make_type(TestConstants.LDP_INDIRECT), 'Content-type': 'text/turtle' } - indirect_body = "@prefix ldp: .\n" \ - "@prefix dc: .\n" \ + indirect_body = "@prefix ldp: <{0}> .\n" \ + "@prefix dc: <{1}> .\n" \ "<> ldp:hasMemberRelation ldp:contains ;\n" \ - "dc:title \"Members Container\" ." + "dc:title \"Members Container\" .".format(TestConstants.LDP_NS, TestConstants.DC_NS) r = self.do_post(self.getBaseUri(), headers=headers, body=indirect_body) self.assertEqual(409, r.status_code, "Did not get expected status code") @@ -222,10 +236,10 @@ def testIndirectWithServerManaged(self): 'Link': self.make_type(TestConstants.LDP_INDIRECT), 'Content-type': 'text/turtle' } - indirect_body = "@prefix ldp: .\n" \ - "@prefix dc: .\n" \ + indirect_body = "@prefix ldp: <{1}> .\n" \ + "@prefix dc: <{2}> .\n" \ "<> ldp:insertedContentRelation <{0}> ;\n" \ "ldp:hasMemberRelation ldp:contains ;\n" \ - "dc:title \"Members Container\" .".format(location) + "dc:title \"Members Container\" .".format(location, TestConstants.LDP_NS, TestConstants.DC_NS) r = self.do_post(self.getBaseUri(), headers=headers, body=indirect_body) self.assertEqual(409, r.status_code, "Did not get expected status code") diff --git a/rdf_tests.py b/rdf_tests.py index 74abbd0..8dfa225 100644 --- a/rdf_tests.py +++ b/rdf_tests.py @@ -3,7 +3,7 @@ import rdflib.parser -import TestConstants +import TestConstants as TC from abstract_fedora_tests import FedoraTests, register_tests, Test @@ -23,6 +23,7 @@ class FedoraRdfTests(FedoraTests): @Test def testRdfSerialization(self): + """ Test GETting RDF in supported formats """ self.log("Put new resource.") r = self.createBasicContainer(self.getBaseUri()) self.assertEqual(201, r.status_code, "Did not create new object") @@ -32,10 +33,10 @@ def testRdfSerialization(self): self.assertTitleExists("An Object", location) self.log("PUT to update title.") - n_triples = "<{0}> \"Updated Title\" .".format(location) + n_triples = "<{0}> <{1}> \"Updated Title\" .".format(location, TC.DC_TITLE) headers = { "Content-type": "application/n-triples", - "Prefer": TestConstants.PUT_PREFER_LENIENT + "Prefer": TC.PUT_PREFER_LENIENT } r = self.do_put(location, headers=headers, body=n_triples) self.assertEqual(204, r.status_code, "Did not update the resource") @@ -64,22 +65,23 @@ def testRdfSerialization(self): @Test def TestRoundtrippingBinary(self): + """ Test roundtrip the metadata from a binary """ self.log("Post new binary") r = self.do_post(headers={'Content-type': 'text/plain'}, body="Content") - self.assertEqual(TestConstants.CREATED, r.status_code, "Did not create binary") + self.assertEqual(TC.CREATED, r.status_code, "Did not create binary") location = self.get_location(r) self.log("location is {}".format(location)) - body = self.do_get(location + "/" + TestConstants.FCR_METADATA, headers={'Accept': 'application/n-triples'}) + body = self.do_get(location + "/" + TC.FCR_METADATA, headers={'Accept': 'application/n-triples'}) graph = rdflib.Graph() graph.parse(data=body.content.decode(encoding='utf-8'), format='nt') for (s, p, o) in graph: - if p in [rdflib.URIRef(TestConstants.FEDORA_NS + "hasFixityService"), - rdflib.URIRef(TestConstants.FEDORA_NS + "created"), - rdflib.URIRef(TestConstants.FEDORA_NS + "createdBy"), - rdflib.URIRef(TestConstants.FEDORA_NS + "lastModified"), - rdflib.URIRef(TestConstants.FEDORA_NS + "lastModifiedBy"), - rdflib.URIRef(TestConstants.RDF_TYPE), + if p in [rdflib.URIRef(TC.FEDORA_NS + "hasFixityService"), + rdflib.URIRef(TC.FEDORA_NS + "created"), + rdflib.URIRef(TC.FEDORA_NS + "createdBy"), + rdflib.URIRef(TC.FEDORA_NS + "lastModified"), + rdflib.URIRef(TC.FEDORA_NS + "lastModifiedBy"), + rdflib.URIRef(TC.RDF_TYPE), rdflib.URIRef("http://www.loc.gov/premis/rdf/v1#hasMessageDigest")]: graph.remove((s, p, o)) @@ -88,25 +90,25 @@ def TestRoundtrippingBinary(self): self.log("Delete binary") r = self.do_delete(location) - self.assertEqual(TestConstants.NO_CONTENT, r.status_code, "Unable to delete binary") + self.assertEqual(TC.NO_CONTENT, r.status_code, "Unable to delete binary") self.log("Delete binary tombstone") - r = self.do_delete(location + "/" + TestConstants.FCR_TOMBSTONE) - self.assertEqual(TestConstants.NO_CONTENT, r.status_code, "Unable to delete binary tombstone") + r = self.do_delete(location + "/" + TC.FCR_TOMBSTONE) + self.assertEqual(TC.NO_CONTENT, r.status_code, "Unable to delete binary tombstone") time.sleep(1) self.log("Put binary back") r = self.do_put(location, headers={'Content-type': 'text/plain'}, body="Content") - self.assertEqual(TestConstants.CREATED, r.status_code, "Did not create binary") + self.assertEqual(TC.CREATED, r.status_code, "Did not create binary") self.log("Put the description back") - r = self.do_put(location + "/" + TestConstants.FCR_METADATA, headers={ + r = self.do_put(location + "/" + TC.FCR_METADATA, headers={ 'Content-type': 'application/n-triples', 'Prefer': 'handling=lenient' }, body=new_body) - self.assertEqual(TestConstants.NO_CONTENT, r.status_code, "Did not update binary description") + self.assertEqual(TC.NO_CONTENT, r.status_code, "Did not update binary description") self.log("Get the body again") - r = self.do_get(location + "/" + TestConstants.FCR_METADATA, headers={'Accept': 'application/n-triples'}) - self.assertEqual(TestConstants.OK, r.status_code, "Could not get the binary description") + r = self.do_get(location + "/" + TC.FCR_METADATA, headers={'Accept': 'application/n-triples'}) + self.assertEqual(TC.OK, r.status_code, "Could not get the binary description") get_body = r.content.decode(encoding='utf-8') self.log("GET body {}".format(get_body)) graph2 = rdflib.Graph() diff --git a/sparql_tests.py b/sparql_tests.py index 4403377..e50564c 100644 --- a/sparql_tests.py +++ b/sparql_tests.py @@ -21,6 +21,7 @@ class FedoraSparqlTests(FedoraTests): @Test def doSparqlContainerTest(self): + """ Test Patch requests to a container """ self.log("Create container") headers = { 'Content-type': 'text/turtle' @@ -44,6 +45,7 @@ def doSparqlContainerTest(self): @Test def doSparqlBinaryTest(self): + """ Test Patch requests to a binary """ self.log("Create a binary") headers = { 'Content-type': 'image/jpeg' @@ -69,6 +71,7 @@ def doSparqlBinaryTest(self): @Test def doUnicodeSparql(self): + """ Test unicode patch requests to a container """ self.log("Create a container") r = self.do_post(self.getBaseUri()) self.checkResponse(201, r) @@ -88,6 +91,7 @@ def doUnicodeSparql(self): @Test def doAddType(self): + """ Test we can add a rdf:type to a resource """ self.log("Create a container") r = self.do_post(self.getBaseUri()) self.checkResponse(201, r) @@ -107,6 +111,7 @@ def doAddType(self): @Test def doAddRestrictedType(self): + """ Test we can't add a rdf:type with a restricted prefix """ self.log("Create a container") r = self.do_post(self.getBaseUri()) self.checkResponse(201, r) @@ -125,6 +130,7 @@ def doAddRestrictedType(self): @Test def doInboundReferenceContainer(self): + """ Test that we can see inbound references in the RDF of a container """ reference = "http://awoods.com/pointer" self.log("Create a container") r = self.do_post(self.getBaseUri()) @@ -158,6 +164,7 @@ def doInboundReferenceContainer(self): @Test def doInboundReferenceBinary(self): + """ Test that we can see inbound references in the RDF of a binary description """ reference = "http://awoods.com/pointer" self.log("Create a binary") headers = { @@ -199,6 +206,7 @@ def doInboundReferenceBinary(self): @Test def testInboundReferenceToSelf(self): + """ Test we can generate a self-referencing inbound reference """ reference = "http://awoods.com/pointsTo" r = self.do_post(self.getBaseUri()) self.checkResponse(TC.CREATED, r) @@ -221,3 +229,9 @@ def testInboundReferenceToSelf(self): json_body = json.loads(body) result = pyjq.all('.[] | select(."@id" == "{}") | ."{}" '.format(location, reference), json_body) self.assertEqual(1, len(result)) + self_reference = pyjq.first( + '.[] | select(."@id" == "{}") | ."{}" | .[0]."@id"'.format(location, reference), + json_body + ) + self.assertEqual(location, self_reference) + diff --git a/ssearch_tests.py b/ssearch_tests.py index 52ff658..dc0b735 100644 --- a/ssearch_tests.py +++ b/ssearch_tests.py @@ -41,6 +41,7 @@ def getSearch(self): @Test def testSearchAll(self): + """ Test that a search returns all resources created """ max_results = 20 offset = 0 items = self.createSomeResources(count=5) @@ -65,6 +66,7 @@ def testSearchAll(self): @Test def testWithBadParameter(self): + """ Test that a bad parameter returns a 400 status """ search = self.getSearch() search.add_condition("myField", "=", "*") r = search.do_query() diff --git a/transaction_tests.py b/transaction_tests.py index 8b592ac..8c017f2 100644 --- a/transaction_tests.py +++ b/transaction_tests.py @@ -28,6 +28,7 @@ def checkResponse(self, expected, response, tx_id=None): @Test def doCommitTest(self): + """ Test creating and committing a transaction """ tx_provider = self.get_transaction_provider() if tx_provider is None: self.log("Could not location transaction provider") @@ -102,6 +103,7 @@ def doCommitTest(self): @Test def doRollbackTest(self): + """ Test creating and rolling back a transaction """ tx_provider = self.get_transaction_provider() if tx_provider is None: self.log("Could not location transaction provider") @@ -144,7 +146,7 @@ def doRollbackTest(self): @Test def createAndDeleteInTwoTransaction(self): - + """ Test creating a resource in one long running transaction and deleting it in a second """ self.log("Create a transaction") tx_id = self.createTransaction() @@ -196,7 +198,7 @@ def createAndDeleteInTwoTransaction(self): @Test def createAndDeleteInOneTransaction(self): - + """ Test creating and deleting a resource in a single transaction """ tx_id = self.createTransaction() self.log("Create a container") @@ -225,6 +227,7 @@ def createAndDeleteInOneTransaction(self): @Test def testTransactionExclusion(self): + """ Test completely removing a resource and then re-adding it in a single transaction """ self.log("Create a container.") r = self.do_post() self.checkResponse(TC.CREATED, r) @@ -269,6 +272,7 @@ def testTransactionExclusion(self): @Test def aPlainUserTransactionRollbackInternal(self): + """ Test a normal user performing actions in a transaction, then rolling it back using info:fedora URIs """ child, tx_id = self.setup_user_writeable_tx() self.log("Rollback transaction") @@ -286,6 +290,7 @@ def aPlainUserTransactionRollbackInternal(self): @Test def aPlainUserTransactionRollbackExternal(self): + """ Test a normal user performing actions in a transaction, then rolling it back using http Fedora URIs """ child, tx_id = self.setup_user_writeable_tx(fedora_base_uri=self.getFedoraBase()) self.log("Rollback transaction") @@ -303,6 +308,7 @@ def aPlainUserTransactionRollbackExternal(self): @Test def aPlainUserTransactionCommitInternal(self): + """ Test a normal user performing actions in a transaction, then committing it using info:fedora URIs """ child, tx_id = self.setup_user_writeable_tx() self.log(f"Test getting the child ({child}) outside the transaction") @@ -319,6 +325,7 @@ def aPlainUserTransactionCommitInternal(self): @Test def aPlainUserTransactionCommitExternal(self): + """ Test a normal user performing actions in a transaction, then committing it using http Fedora URIs """ child, tx_id = self.setup_user_writeable_tx(fedora_base_uri=self.getFedoraBase()) self.log(f"Test getting the child ({child}) outside the transaction") @@ -335,6 +342,8 @@ def aPlainUserTransactionCommitExternal(self): @Test def aSinglePlainUserTransactionCommitInternal(self): + """ Test a normal user performing actions in a transaction, but a second user doesn't have permission + to the transaction endpoint using info:fedora URIs """ child, tx_id = self.single_user_tx_setup() self.log("Commit transaction") @@ -352,23 +361,8 @@ def aSinglePlainUserTransactionCommitInternal(self): @Test def aSinglePlainUserTransactionCommitExternal(self): - child, tx_id = self.single_user_tx_setup(fedora_base_uri=self.getFedoraBase()) - - self.log("Commit transaction") - r = self.do_put(tx_id, admin=False) - self.checkResponse(TC.NO_CONTENT, r, tx_id) - - self.log(f"Test getting the child ({child}) outside the transaction") - r = self.do_get(child, admin=False) - self.checkResponse(TC.OK, r, tx_id) - - self.log("Try to start a transaction as the second normal user") - tx_endpoint = self.get_transaction_provider() - r = self.do_post(tx_endpoint, admin=self.create_user2_auth()) - self.checkResponse(TC.FORBIDDEN, r) - - @Test - def aSinglePlainUserTransactionCommitExternal(self): + """ Test a normal user performing actions in a transaction, but a second user doesn't have permission + to the transaction endpoint using http: Fedora URIs """ child, tx_id = self.single_user_tx_setup(fedora_base_uri=self.getFedoraBase()) self.log("Commit transaction") diff --git a/version_tests.py b/version_tests.py index b3d75c9..5d5b772 100644 --- a/version_tests.py +++ b/version_tests.py @@ -184,6 +184,7 @@ def makeVersionsInsideASecond(self): @Test def doBinaryVersioningTest(self): + """ Test versioning with binaries and their metadata containers endpoint are synced """ headers = { 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE), 'Content-Type': 'text/csv' @@ -218,7 +219,7 @@ def doBinaryVersioningTest(self): r = self.do_post(version_endpoint) self.checkResponse(TestConstants.CREATED, r) - self.log("Try to create another version within a second") + self.log("Try to create another version within a second but not at the same time") r = self.do_post(version_endpoint) self.checkResponse(TestConstants.CREATED, r) @@ -287,6 +288,7 @@ def doBinaryVersioningTest(self): @Test def checkBinaryVersioning(self): + """ Test that changes to a binary are displayed on the binary description version endpoint too """ headers = { 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE), 'Content-Type': 'text/csv' @@ -343,7 +345,8 @@ def checkBinaryVersioning(self): self.checkMementoCount(3, metadata_versions) @Test - def createBinaryVersionsAtSameTime(self): + def createBinaryVersionsWithTimeOrBody(self): + """ Test we can no longer provide a version datetime or a version body """ headers = { 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE), 'Content-Type': 'text/csv' @@ -357,8 +360,8 @@ def createBinaryVersionsAtSameTime(self): description_location = self.find_binary_description(r) r = self.do_get(description_location) - new_body = "@prefix dc: <{0}> .\n".format(TestConstants.PURL_NS) + \ - r.text[0:-2] + ";\n dc:title \"New title\" .\n".format(TestConstants.PURL_NS) + new_body = "@prefix dc: <{0}> .\n".format(TestConstants.DC_NS) + \ + r.text[0:-2] + ";\n dc:title \"New title\" .\n".format(TestConstants.DC_NS) version_endpoint = location + "/" + TestConstants.FCR_VERSIONS description_version_endpoint = description_location + "/" + TestConstants.FCR_VERSIONS @@ -401,6 +404,7 @@ def createBinaryVersionsAtSameTime(self): @Test def testMementoAreAccessibleAfterDelete(self): + """ Test mementos are still accessible when a resource is deleted but not purged. """ r = self.do_post() self.checkResponse(TestConstants.CREATED, r) uri = self.get_location(r) @@ -425,6 +429,7 @@ def testMementoAreAccessibleAfterDelete(self): @Test def testBinaryDescription(self): + """ Test checking past versions of binary descriptions """ headers = { 'Content-type': 'text/plain' } From 636b6a1c806ba4d9d72b385d42840b95a251d1ce Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Wed, 11 Sep 2024 15:56:11 -0500 Subject: [PATCH 20/20] Add some range tests --- TestConstants.py | 2 ++ abstract_fedora_tests.py | 6 +++++ basic_interaction_tests.py | 50 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/TestConstants.py b/TestConstants.py index 28c43d7..e3bc964 100644 --- a/TestConstants.py +++ b/TestConstants.py @@ -97,6 +97,7 @@ CREATED = 201 NO_CONTENT = 204 OK = 200 +PARTIAL_CONTENT = 206 NOT_AUTHORIZED = 401 FORBIDDEN = 403 NOT_FOUND = 404 @@ -104,6 +105,7 @@ GONE = 410 BAD_REQUEST = 400 METHOD_NOT_ALLOWED = 405 +UNSATISFIABLE_RANGE = 416 SERVER_ERROR = 500 diff --git a/abstract_fedora_tests.py b/abstract_fedora_tests.py index 9234403..da155a6 100644 --- a/abstract_fedora_tests.py +++ b/abstract_fedora_tests.py @@ -254,6 +254,12 @@ def check_for_retest(self, uri: str): quit() def getHeader(self, uri: str, header_name: str, headers: dict = None): + """ Perform a HEAD request and return the requested header if exists. + :param uri (str) - The URI to request + :param header_name (str) - The header to look for + :param headers (dict) - headers to provide for the HEAD request or None for none. + :return the header(s) or None + """ if headers is None: headers = {} r = self.do_head(uri, headers=headers) diff --git a/basic_interaction_tests.py b/basic_interaction_tests.py index b674c18..f4a32d5 100644 --- a/basic_interaction_tests.py +++ b/basic_interaction_tests.py @@ -1,5 +1,7 @@ #!/bin/env python +import random import shutil +import string import tempfile import time @@ -578,3 +580,51 @@ def testDeleteAndPutOverTombstoneWrongTypeNonRdf(self): TC.OVERWRITE_TOMBSTONE_HEADER: "true" }) self.checkResponse(TC.CONFLICT, r) + + @Test + def testRangeRequests(self): + """ Test that we support RFC 7233 range requests """ + self.log("Create a NonRDFResource") + r = self.do_post(headers={ + 'Link': self.make_type(TC.LDP_NON_RDF_SOURCE), + 'Content-type': 'text/plain' + }, body=''.join(random.choices(string.ascii_lowercase, k=100))) + self.checkResponse(TC.CREATED, r) + location = self.get_location(r) + length = self.getHeader(location, 'Content-Length') + if length is not None: + half_length = int(int(length) / 2) + one_longer = int(length) + 1 + + self.log("Perform request for first half of the range") + r = self.do_get(location, headers={ + 'Range': 'bytes=0-{0}'.format(half_length) + }) + self.checkResponse(TC.PARTIAL_CONTENT, r) + range = r.headers['Content-Range'] + if range: + self.assertEqual('bytes 0-{0}/{1}'.format(half_length, length), range) + else: + self.fail("No Content-Range header found") + + self.log("Perform request for range slightly longer than the content") + r = self.do_get(location, headers={ + 'Range': 'bytes=0-{0}'.format(one_longer) + }) + self.checkResponse(TC.PARTIAL_CONTENT, r) + range = r.headers['Content-Range'] + if range: + self.assertEqual('bytes 0-{0}/{1}'.format(int(length) - 1, length), range) + else: + self.fail("No Content-Range header found") + + self.log("Perform request for range of the content length") + r = self.do_get(location, headers={ + 'Range': 'bytes=0-{0}'.format(length) + }) + self.checkResponse(TC.PARTIAL_CONTENT, r) + range = r.headers['Content-Range'] + if range: + self.assertEqual('bytes 0-{0}/{1}'.format(int(length) -1, length), range) + else: + self.fail("No Content-Range header found")