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/README.md b/README.md
index 27835f8..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
@@ -68,6 +63,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
@@ -80,13 +77,16 @@ 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.
-* `authz` - Authorization tests
+* `archivalgroup` - Archival Group tests
+* `authz` - Authorization tests (see [note](#authz-tests))
* `basic` - Basic interaction tests
-* `sparql` - Sparql operation tests
+* `camel` - Camel toolbox tests (see [note](#camel-tests))
+* `fixity` - Binary fixity tests
+* `indirect` - Direct/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,8 +95,35 @@ 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.
+
+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
+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
@@ -108,6 +135,34 @@ To run only the `authz` and `sparql` tests you would execute:
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
@@ -121,11 +176,39 @@ To run only the `authz` and `sparql` tests you would execute:
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
+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 +220,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 +253,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..e3bc964 100644
--- a/TestConstants.py
+++ b/TestConstants.py
@@ -10,15 +10,32 @@
USER2_PASS_PARAM = "password2"
LOG_FILE_PARAM = "logfile"
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"
-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"
+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_API_NS + "PreferInboundReferences"
+EMBEDED_RESOURCE = FEDORA_NS + "EmbedResources"
+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"
GET_PREFER_MINIMAL = "return=minimal"
PUT_PREFER_LENIENT = "handling=lenient; received=\"minimal\""
@@ -29,8 +46,14 @@
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"
+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"
@@ -44,9 +67,45 @@
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#"
+
+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: <{1}> ." \
+ "@prefix pcdm: <{2}> ." \
+ "<> a pcdm:Object ;" \
+ "dc:title \"{0}\" .".format(PCDM_CONTAINER_TITLE, DC_NS, PCDM_NS)
+# Standard Response codes
+CREATED = 201
+NO_CONTENT = 204
+OK = 200
+PARTIAL_CONTENT = 206
+NOT_AUTHORIZED = 401
+FORBIDDEN = 403
+NOT_FOUND = 404
+CONFLICT = 409
+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 1e500c2..da155a6 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,16 @@ class FedoraTests(unittest.TestCase):
# Holds the configuration
config = {}
+ # 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:
+ config['debug_level'] = 1
self.config = config
def getBaseUri(self):
@@ -33,14 +42,33 @@ 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))
""" Check we can access the machine and then run the tests """
self.not_authorized()
for test in self._testdict:
method = getattr(self, test)
- method()
+ self.log("Running {0}".format(test))
+ 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))
def not_authorized(self):
""" Ensure we can even access the repository """
@@ -56,6 +84,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 """
@@ -71,6 +110,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
@@ -138,6 +182,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
@@ -146,15 +191,18 @@ 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(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 """
@@ -162,7 +210,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)
@@ -179,32 +227,54 @@ 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 """
- 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()
+
+ 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)
+ 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 = {}
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)]
@@ -262,14 +332,81 @@ 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)
+ 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))
+
+ def assertContrainedByHeaderExists(self, response):
+ """ Check for link headers with the constrainedby rel type """
+ self.assertLinkHeaderExists(response, "http://www.w3.org/ns/ldp#constrainedBy")
+
def log(self, message):
- print(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)
self.assertIsNotNone(headers['describedby'])
return headers['describedby'][0]
+ 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}")
+ self.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 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/archival_group_tests.py b/archival_group_tests.py
new file mode 100644
index 0000000..ec8b130
--- /dev/null
+++ b/archival_group_tests.py
@@ -0,0 +1,217 @@
+#!/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):
+ """ 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)
+ })
+ 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):
+ """ 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)
+ })
+ 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):
+ """ 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)
+ })
+ 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):
+ """ 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)
+ })
+ 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):
+ """ 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)
+ })
+ 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):
+ """ 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)
+ })
+ 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 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"
+ })
+ self.checkResponse(TC.CREATED, r)
+
+ @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)
+ })
+ 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"
+ })
+ self.checkResponse(TC.CONFLICT, r)
diff --git a/authz_tests.py b/authz_tests.py
index acf40ca..569c78f 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"
@@ -34,208 +33,603 @@ 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")
-
- def getAclUri(self, response):
- acls = self.get_link_headers(response)
- self.assertIsNotNone(acls['acl'])
- return acls['acl'][0]
+ if TC.NOT_AUTHORIZED != r.status_code:
+ self.log("It appears that authentication is not enabled on your repository.")
+ quit()
+
+ @staticmethod
+ def getAclUri(response):
+ acls = FedoraAuthzTests.get_link_headers(response)
+ try:
+ return acls['acl'][0]
+ except KeyError:
+ Exception("No acl link header found")
@Test
def doAuthTests(self):
- self.log("Running doAuthTests")
-
+ """ Basic permissions test """
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")
+ 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 = self.getAclUri(r)
+ cover_acl = FedoraAuthzTests.getAclUri(r)
self.log("Make \"cover\" a pcdm:Object")
sparql = "PREFIX pcdm: " \
"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.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(TC.NOT_FOUND, r)
self.log("Add ACL to \"cover\"")
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.assertEqual(201, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.CREATED, 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")
+ self.checkResponse(TC.CREATED, r)
files_location = self.get_location(r)
- files_acl = self.getAclUri(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(TC.NOT_AUTHORIZED, 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(TC.NOT_AUTHORIZED, 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.assertEqual(200, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.OK, 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.assertEqual(200, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.OK, 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.assertEqual(200, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.OK, 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.assertEqual(200, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.OK, 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.assertEqual(403, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.FORBIDDEN, 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.assertEqual(403, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.FORBIDDEN, 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(TC.NOT_FOUND, 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.assertEqual(201, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.CREATED, 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.assertEqual(403, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.FORBIDDEN, 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.assertEqual(200, r.status_code, "Did not get expected response code")
-
- self.log("Passed")
+ self.checkResponse(TC.OK, r)
@Test
def doDirectIndirectAuthTests(self):
- self.log("Running doDirectIndirectAuthTests")
-
+ """ Test that direct and indirect containers require permissions to the ldp:membershipResource too """
self.verifyAuthEnabled()
self.log("Create a target container")
r = self.do_post()
- self.assertEqual(201, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.CREATED, r)
target_location = self.get_location(r)
- target_acl = self.getAclUri(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")
+ self.checkResponse(TC.CREATED, r)
write_location = self.get_location(r)
- write_acl = self.getAclUri(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[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.assertEqual(201, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.CREATED, r)
- self.log("Make sure the write resource is writable by \"{0}\"".format(self.config[TestConstants.USER_NAME_PARAM]))
- write_ttl = "@prefix acl: .\n" \
+ 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)
+ " 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.assertEqual(201, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.CREATED, 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.assertEqual(201, r.status_code, "Did not get expected response code")
+ 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[TestConstants.USER_NAME_PARAM]))
+ self.config[TC.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(TC.CREATED, 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.assertEqual(403, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.FORBIDDEN, 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: .\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(TC.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(TC.FORBIDDEN, r)
headers = {
'Content-type': 'text/turtle',
- 'Link': self.make_type(TestConstants.LDP_INDIRECT)
+ 'Link': self.make_type(TC.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(TC.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(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.assertEqual(201, r.status_code, "Did not get expected response code")
+ self.checkResponse(TC.CREATED, 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(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[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")
+ 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[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.assertEqual(201, r.status_code, "Did not get expected response code")
+ 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[TestConstants.USER_NAME_PARAM]))
+ self.config[TC.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(TC.CREATED, r)
+
+ @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")
+ r = self.do_post()
+ self.checkResponse(201, r)
+ target_location = self.get_location(r)
+ target_acl = FedoraAuthzTests.getAclUri(r)
+
+ double_ttl = "@prefix acl: <{2}> .\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[TC.USER_NAME_PARAM], target_location,
+ TC.ACL_NS)
+ headers = {
+ 'Content-type': TC.TURTLE_MIMETYPE
+ }
+
+ 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)
+
+ self.log("Check we can read")
+ r = self.do_get(target_location, admin=False)
+ self.checkResponse(200, r)
+
+ self.log("Check we can write")
+ headers = {
+ 'Content-type': TC.SPARQL_UPDATE_MIMETYPE
+ }
+ 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',
+ '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)
+ 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)
+
+ 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)
+
+ @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")
+ headers = {
+ 'Content-type': 'text/plain',
+ '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)
+ 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(TC.ACL_NS, binary_location,
+ self.config[TC.USER_NAME_PARAM])
+ headers = {
+ 'Content-type': TC.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(TC.DC_NS)
+ headers = {
+ 'Content-type': TC.SPARQL_UPDATE_MIMETYPE
+ }
+ r = self.do_patch(binary_description, patch_body, headers=headers, admin=False)
+ self.checkResponse(204, r)
+
+ @Test
+ def testContainerWithAccessToClass(self):
+ """ Test permissions using AccessToClass """
+ self.verifyAuthEnabled()
+
+ self.log("Create a container")
+ r = self.do_post()
+ location = self.get_location(r)
+ acl_location = self.getAclUri(r)
+ 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(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': TC.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(200, r)
+
+ 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):
+ """ Ensure permissions work in transactions and do not extend """
+ 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):
+ """ Test authentication using a group """
+ 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):
+ """ 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)
+
+ 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):
+ """ 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)
+
+ 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):
+ """ 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)
+ 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 4c270dc..f4a32d5 100644
--- a/basic_interaction_tests.py
+++ b/basic_interaction_tests.py
@@ -1,8 +1,16 @@
#!/bin/env python
+import random
+import shutil
+import string
+import tempfile
+import time
-import TestConstants
+import TestConstants as TC
from abstract_fedora_tests import FedoraTests, register_tests, Test
import os
+import pyjq
+import json
+import uuid
@register_tests
@@ -17,85 +25,136 @@ 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").strip()
+
+ def getStateToken(self, uri):
+ """ Get the X-State-Token header for a specified 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 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)
+ self.checkResponse(TC.CREATED, r)
+
+ self.log("Check container exists")
+ r = self.do_head(container_location)
+ self.checkResponse(TC.OK, r)
+ r = self.do_get(container_location)
+ self.checkResponse(TC.OK, r)
+
+ self.log("Delete the container")
+ r = self.do_delete(container_location)
+ self.checkResponse(TC.NO_CONTENT, r)
+
+ self.log("Check container doesn't exists")
+ r = self.do_head(container_location)
+ self.checkResponse(TC.GONE, r)
+ r = self.do_get(container_location)
+ self.checkResponse(TC.GONE, r)
+
+ self.log("Try to put to location held by tombstone")
+ r = self.do_put(container_location)
+ self.checkResponse(TC.GONE, r)
+
+ self.log("Delete the tombstone")
+ r = self.do_delete(container_location + "/" + TC.FCR_TOMBSTONE)
+ self.checkResponse(TC.NO_CONTENT, r)
+
+ self.log("Check container doesn't exists")
+ r = self.do_head(container_location)
+ self.checkResponse(TC.NOT_FOUND, r)
+ r = self.do_get(container_location)
+ self.checkResponse(TC.NOT_FOUND, r)
+
+ self.log("Try to put to location again")
+ r = self.do_put(container_location)
+ self.checkResponse(TC.CREATED, r)
+
@Test
def testBasicContainer(self):
- self.log("Running testBasicContainer")
- self.createTestResource(TestConstants.LDP_BASIC)
- self.log("Passed")
+ """ Test creating a basic container """
+ self.createTestResource(TC.LDP_BASIC)
@Test
def testDirectContainer(self):
- self.log("Running testDirectContainer")
- self.createTestResource(TestConstants.LDP_DIRECT)
- self.log("Passed")
+ """ Test creating a direct container """
+ self.createTestResource(TC.LDP_DIRECT)
@Test
def testIndirectContainer(self):
- self.log("Running testIndirectContainer")
- self.createTestResource(TestConstants.LDP_INDIRECT)
- self.log("Passed")
+ """ Test creating an indirect container """
+ self.createTestResource(TC.LDP_INDIRECT)
@Test
def testNonRdfSource(self):
- self.log("Running testNonRdfSource")
+ """ Test creating a binary """
testfiles = {'files': ('testdata.csv', 'this,is,some,data\n')}
- self.createTestResource(TestConstants.LDP_NON_RDF_SOURCE, files=testfiles)
- self.log("Passed")
+ 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 """
- self.log("Running testLdpResource")
- link_type = self.make_type(TestConstants.LDP_RESOURCE)
+ link_type = self.make_type(TC.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")
+ self.checkResponse(TC.BAD_REQUEST, r)
@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)
+ link_type = self.make_type(TC.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")
-
-
+ self.checkResponse(TC.BAD_REQUEST, r)
@Test
def doNestedTests(self):
- self.log("Running doNestedTests")
-
+ """ Test creating child objects and removing them """
self.log("Create a container")
- headers = {
- 'Content-type': 'text/turtle'
- }
- r = self.do_post(self.getBaseUri(), headers=headers, body=TestConstants.OBJECT_TTL)
- self.assertEqual(201, r.status_code, "Did not get expected status code")
+ r = self.createBasicContainer(self.getBaseUri())
+ self.checkResponse(TC.CREATED, r)
location = self.get_location(r)
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)
+ r = self.createBasicContainer(location)
+ self.checkResponse(TC.CREATED, 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:
@@ -103,29 +162,111 @@ def doNestedTests(self):
'Content-type': 'image/jpeg'
}
data = fp.read()
- r = self.do_post(location2, headers=headers, body=data)
- self.assertEqual(201, r.status_code, "Did not get expected status code")
+ r = self.do_post(main_child1, headers=headers, body=data)
+ 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.checkResponse(TC.CREATED, r)
+ main_child2 = self.get_location(r)
+
+ self.log("Verify containment")
+ headers = {
+ 'Accept': TC.JSONLD_MIMETYPE
+ }
+ r = self.do_get(location, headers=headers)
+ 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)
+ 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")
+ 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(location2)
- self.assertEqual(410, r.status_code, "Did not get expected status code")
+ r = self.do_get(main_child1)
+ 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):
+ """ Test create, delete and purge a container """
+ 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)
- self.log("Passed")
+ 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):
+ """ Test create, delete and purge a binary """
+ 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}.
@@ -135,39 +276,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
@@ -177,27 +318,313 @@ 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")
+ self.checkResponse(result, r)
@Test
def testChangeIxnModel(self):
- self.log("Running changeIxnModel")
-
+ """ Test responses when trying to change interaction models """
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)
+
+ @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',
+ 'Content-Disposition': 'attachment; filename="mytestfile.txt"'
+ }
+ r = self.do_post(self.getBaseUri(), headers=headers, body="some sample text")
+ self.checkResponse(TC.CREATED, r)
+
+ @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)
+ parent_uri = self.get_location(r)
+
+ first_etag = self.getEtag(parent_uri)
+ 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)
+ 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):
+ r = self.do_post(parent_uri, {'Slug': 'child_' + str(i)})
+ self.checkResponse(TC.CREATED, r)
+
+ third_etag = self.getEtag(parent_uri)
+ 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")
+ 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)
+
+ # @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")
+
+ @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)
+ 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):
+ """ 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),
+ '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):
+ """ 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)
+ 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):
+ """ 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),
+ '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)
+
+ @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")
diff --git a/camel_tests.py b/camel_tests.py
new file mode 100644
index 0000000..fe27616
--- /dev/null
+++ b/camel_tests.py
@@ -0,0 +1,137 @@
+#!/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 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
+ 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/fixity_tests.py b/fixity_tests.py
index 30d4244..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
@@ -14,34 +10,40 @@ 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):
-
+ """ Test doing a fixity test and that it matches the expected result. """
self.log("Create a binary")
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')
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.")
-
- self.log("Passed")
\ No newline at end of file
+ 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 bd9fcea..f829b5c 100644
--- a/indirect_tests.py
+++ b/indirect_tests.py
@@ -4,18 +4,17 @@
from abstract_fedora_tests import FedoraTests, register_tests, Test
import json
import pyjq
+import uuid
@register_tests
class FedoraIndirectTests(FedoraTests):
-
# Create test objects all inside here for ease of review/removal
CONTAINER = "/test_indirect"
@Test
def doPcdmIndirect(self):
- self.log("Running doPcdmIndirect")
-
+ """ Test that we can create a working indirect container """
self.log("Create a PCDM container")
basic_headers = {
'Link': self.make_type(TestConstants.LDP_BASIC),
@@ -35,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")
@@ -71,4 +75,171 @@ def doPcdmIndirect(self):
found_member = True
self.assertTrue(found_member, "Did not find hasMember property")
- self.log("Passed")
\ No newline at end of file
+ @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
+ }
+ 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: <{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")
+
+ 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 { <> <" + 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")
+
+ 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: <{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")
+
+ 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: <{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,
+ '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, 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: <{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: <{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: <{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')
+
+ 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: <{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")
+
+ 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: <{0}> .\n" \
+ "@prefix dc: <{1}> .\n" \
+ "<> ldp:hasMemberRelation ldp:contains ;\n" \
+ "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")
+
+ @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: <{1}> .\n" \
+ "@prefix dc: <{2}> .\n" \
+ "<> ldp:insertedContentRelation <{0}> ;\n" \
+ "ldp:hasMemberRelation ldp:contains ;\n" \
+ "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 dc65436..8dfa225 100644
--- a/rdf_tests.py
+++ b/rdf_tests.py
@@ -1,6 +1,9 @@
#!/bin/env python
+import time
-import TestConstants
+import rdflib.parser
+
+import TestConstants as TC
from abstract_fedora_tests import FedoraTests, register_tests, Test
@@ -20,13 +23,9 @@ class FedoraRdfTests(FedoraTests):
@Test
def testRdfSerialization(self):
- self.log("Starting testRdfSerialization")
-
+ """ Test GETting RDF in supported formats """
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)
@@ -34,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,4 +63,56 @@ 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
+ @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(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 + "/" + 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(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))
+
+ new_body = graph.serialize(format='nt')
+ self.log("PUTTING body {}".format(new_body))
+
+ self.log("Delete binary")
+ r = self.do_delete(location)
+ self.assertEqual(TC.NO_CONTENT, r.status_code, "Unable to delete binary")
+ self.log("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(TC.CREATED, r.status_code, "Did not create binary")
+ self.log("Put the description back")
+ r = self.do_put(location + "/" + TC.FCR_METADATA, headers={
+ 'Content-type': 'application/n-triples',
+ 'Prefer': 'handling=lenient'
+ }, body=new_body)
+ self.assertEqual(TC.NO_CONTENT, r.status_code, "Did not update binary description")
+
+ self.log("Get the body again")
+ 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()
+ 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 3a7e903..3275541 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
-PyYAML==5.4
-requests==2.20
-pyjq==2.3.0
+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 4a0878c..e50564c 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
@@ -19,35 +21,31 @@ class FedoraSparqlTests(FedoraTests):
@Test
def doSparqlContainerTest(self):
- self.log("Running doSparqlContainerTest")
-
+ """ Test Patch requests to a container """
self.log("Create container")
headers = {
'Content-type': 'text/turtle'
}
- r = self.do_post(self.getBaseUri(), headers=headers, body=TestConstants.OBJECT_TTL)
- self.assertEqual(201, r.status_code, "Did not create container")
+ r = self.do_post(self.getBaseUri(), headers=headers, body=TC.OBJECT_TTL)
+ self.checkResponse(201, r)
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")
+ 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)
- self.log("Passed")
-
@Test
def doSparqlBinaryTest(self):
- self.log("Running doSparqlBinaryTest")
-
+ """ Test Patch requests to a binary """
self.log("Create a binary")
headers = {
'Content-type': 'image/jpeg'
@@ -55,31 +53,28 @@ 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")
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")
+ 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)
- self.log("Passed")
-
@Test
def doUnicodeSparql(self):
- self.log("Running doUnicodeSparql")
-
+ """ Test unicode patch requests to a container """
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: " \
@@ -88,10 +83,155 @@ 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")
+ self.checkResponse(204, r)
self.assertTitleExists("Die von Blumenbach gegründete anthropologische Sammlung der Universität", location)
- self.log("Passed")
\ No newline at end of file
+ @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)
+ 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': TC.SPARQL_UPDATE_MIMETYPE
+ }
+ r = self.do_patch(location, headers=headers, body=sparql)
+ self.checkResponse(204, r)
+ self.assertTypeExists("http://www.example.org/ns#type", location)
+
+ @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)
+ location = self.get_location(r)
+
+ sparql = "PREFIX rdf: " \
+ "PREFIX ldp: " \
+ "PREFIX example: " \
+ "INSERT DATA {{ <> rdf:type <{0}> }}".format(TC.LDP_DIRECT)
+ self.log("Patching with our own type")
+ headers = {
+ 'Content-type': TC.SPARQL_UPDATE_MIMETYPE
+ }
+ r = self.do_patch(location, headers=headers, body=sparql)
+ self.checkResponse(409, r)
+
+ @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())
+ 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):
+ """ 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 = {
+ '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):
+ """ 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)
+ 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))
+ 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
new file mode 100644
index 0000000..dc0b735
--- /dev/null
+++ b/ssearch_tests.py
@@ -0,0 +1,73 @@
+#!/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):
+ """ Test that a search returns all resources created """
+ 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):
+ """ Test that a bad parameter returns a 400 status """
+ 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 f7baf14..82d088c 100755
--- a/testrunner.py
+++ b/testrunner.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import argparse
import TestConstants
@@ -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
@@ -17,6 +18,9 @@
from transaction_tests import FedoraTransactionTests
from authz_tests import FedoraAuthzTests
from indirect_tests import FedoraIndirectTests
+from camel_tests import FedoraCamelTests
+from ssearch_tests import FedoraSimpleSearchTests
+from archival_group_tests import FedoraArchivalGroupTests
class FedoraTestRunner:
@@ -31,23 +35,43 @@ 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),
+ ('debug_level', False),
+ ('failed_only', 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,
+ 'archivalgroup': FedoraArchivalGroupTests,
+ }
+
+ def set_up(self, setup_args):
+ self.parse_cmdline_args(setup_args)
self.check_config()
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):
+ 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:
sitename = eval('args.' + TestConstants.SITE_NAME_PARAM)
@@ -73,36 +97,40 @@ 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()
-
- 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
+
+ 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():
+ 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)
+ results = self.run_tests()
+ self.report(results)
def csv_list(string):
@@ -116,7 +144,14 @@ def csv_list(string):
class CSVAction(argparse.Action):
- valid_options = ["authz", "basic", "sparql", "rdf", "version", "transaction", "fixity", "indirect"]
+ """ 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):
@@ -136,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.")
@@ -158,9 +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='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 d271c8e..8c017f2 100644
--- a/transaction_tests.py
+++ b/transaction_tests.py
@@ -1,109 +1,481 @@
#!/bin/env python
-import TestConstants
+import TestConstants as TC
from abstract_fedora_tests import FedoraTests, register_tests, Test
-import json
-import pyjq
@register_tests
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_get(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
+ 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):
- self.log("Running doCommitTest")
-
+ """ Test creating and committing a transaction """
tx_provider = self.get_transaction_provider()
if tx_provider is None:
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")
- full_transaction_uri = self.get_location(r)
- transaction = full_transaction_uri.replace(self.getFedoraBase(), "")
- self.log("Transaction is {0}".format(transaction))
+ transaction_id = self.createTransaction()
+ 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.checkResponse(TC.NO_CONTENT, r, transaction_id)
+ self.assertHeaderExists(r, "Atomic-Expires")
self.log("Create an container in the transaction")
- r = self.do_post(full_transaction_uri + self.CONTAINER)
- self.assertEqual(201, r.status_code, "Did not get expected response code")
+ transaction_headers = {
+ 'Atomic-Id': transaction_id
+ }
+ r = self.do_post(headers=transaction_headers)
+ 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)
- self.assertEqual(200, r.status_code, "Did not get expected response code")
+ r = self.do_get(transaction_obj, headers=transaction_headers)
+ self.checkResponse(TC.OK, r, transaction_id)
self.log("Container not available outside the transaction")
- outside_location = transaction_obj.replace(transaction, "")
- r = self.do_get(outside_location)
- self.assertEqual(404, r.status_code, "Did not get expected response code")
+ r = self.do_get(transaction_obj)
+ 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.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() + "/" + TC.FCR_TX + "/", "")
+ }
+ r = self.do_post(headers=diff_headers)
+ 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.checkResponse(TC.OK, r, transaction_id)
+
+ self.log("Second container not available outside the transaction")
+ r = self.do_get(second_obj)
+ self.checkResponse(TC.NOT_FOUND, r, transaction_id)
+
+ 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_post(full_transaction_uri + "/fcr:tx/fcr: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(outside_location)
- self.assertEqual(200, r.status_code, "Did not get expected response code")
+ r = self.do_get(transaction_obj)
+ self.checkResponse(TC.OK, r)
+
+ self.log("Transaction is no longer available")
+ r = self.do_get(transaction_id)
+ self.checkResponse(TC.GONE, r)
- self.log("Passed")
+ self.log("Can't use the transaction anymore")
+ r = self.do_post(headers=transaction_headers)
+ self.checkResponse(TC.CONFLICT, r)
@Test
def doRollbackTest(self):
- self.log("Running doRollbackTest")
-
+ """ 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")
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")
- full_transaction_uri = self.get_location(r)
- transaction = full_transaction_uri.replace(self.getFedoraBase(), "")
- self.log("Transaction is {0}".format(transaction))
+ transaction_id = self.createTransaction()
+ 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)
- self.assertEqual(201, r.status_code, "Did not get expected response code")
+ transaction_headers = {
+ 'Atomic-Id': transaction_id
+ }
+ r = self.do_post(headers=transaction_headers)
+ 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)
- self.assertEqual(200, r.status_code, "Did not get expected response code")
+ r = self.do_get(transaction_obj, headers=transaction_headers)
+ self.checkResponse(TC.OK, r, transaction_id)
self.log("Container not available outside the transaction")
- outside_location = transaction_obj.replace(transaction, "")
- r = self.do_get(outside_location)
- self.assertEqual(404, r.status_code, "Did not get expected response code")
+ r = self.do_get(transaction_obj)
+ self.checkResponse(TC.NOT_FOUND, r, transaction_id)
self.log("Rollback transaction")
- r = self.do_post(full_transaction_uri + "/fcr:tx/fcr:rollback")
- self.assertEqual(204, r.status_code, "Did not get expected response code")
+ r = self.do_delete(transaction_id)
+ self.checkResponse(TC.NO_CONTENT, r, transaction_id)
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")
+ r = self.do_get(transaction_obj)
+ self.checkResponse(TC.NOT_FOUND, r)
+
+ self.log("Transaction is no longer available")
+ r = self.do_get(transaction_id)
+ self.checkResponse(TC.GONE, r)
+
+ self.log("Can't use the transaction anymore")
+ r = self.do_post(headers=transaction_headers)
+ self.checkResponse(TC.CONFLICT, r)
+
+ @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()
+
+ 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):
+ """ Test creating and deleting a resource in a single 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: {}".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):
+ """ 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)
+ 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)
+
+ @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")
+ 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):
+ """ 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")
+ 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):
+ """ 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")
+ 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):
+ """ 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")
+ 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):
+ """ 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")
+ 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")
+ 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)
+ 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("Passed")
+ 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(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(f"Test getting the child ({child}) outside the transaction")
+ return child, tx_id
diff --git a/version_tests.py b/version_tests.py
index f6e1e4e..5d5b772 100644
--- a/version_tests.py
+++ b/version_tests.py
@@ -1,8 +1,21 @@
#!/bin/env python
+import collections
+import json
+
+import pyjq
import TestConstants
from abstract_fedora_tests import FedoraTests, register_tests, Test
import time
+import rdflib
+from rdflib.namespace import DC, RDF
+import concurrent.futures as futures
+
+
+def get_version_endpoint(uri):
+ if not uri.endswith("/" + TestConstants.FCR_VERSIONS):
+ uri += "/" + TestConstants.FCR_VERSIONS
+ return uri
@register_tests
@@ -12,63 +25,97 @@ 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, 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': rdf_type
+ }
+ r = self.do_get(uri, headers=headers, admin=admin)
+ self.checkResponse(200, 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:
+ admin = True
+ uri = get_version_endpoint(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):
- self.log("Running doContainerVersioningTest")
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(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.assertEqual(200, r.status_code, "Did not find versions where we should")
+ self.checkResponse(TestConstants.OK, 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(TestConstants.CREATED, r)
+ memento_location = self.get_location(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")
- self.log("Create a version with provided 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="[]")
- self.assertEqual(400, r.status_code, "Empty body should cause Bad request")
-
- r = self.do_post(version_endpoint, headers=headers, body=body)
- self.assertEqual(201, r.status_code, "Did not create memento will body.")
- 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.assertEqual(409, r.status_code, "Did not get conflict trying to create a second memento")
-
- self.log("Check memento exists")
- r = self.do_get(memento_location)
- self.assertEqual(200, r.status_code, "Memento was not found")
- 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: " \
@@ -79,59 +126,65 @@ 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(TestConstants.NO_CONTENT, 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(TestConstants.METHOD_NOT_ALLOWED, 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")
r = self.do_post(version_endpoint)
- self.assertEqual(201, r.status_code, 'Did not create a version')
+ self.checkResponse(TestConstants.CREATED, r)
self.log("Count mementos")
- headers = {
- 'Accept': 'application/link-format'
- }
- r = self.do_get(version_endpoint, headers=headers)
- self.assertEqual(200, r.status_code, "Did not get the fcr:versions content")
- self.assertEqual(3, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos")
+ self.checkMementoCount(2, version_endpoint)
- self.log("Delete a Memento")
+ self.log("Try to delete a Memento")
r = self.do_delete(memento_location)
- self.assertEqual(204, r.status_code, "Did not delete the Memento")
+ self.checkResponse(TestConstants.METHOD_NOT_ALLOWED, r)
- self.log("Validate delete")
+ self.log("Check memento still exists.")
r = self.do_get(memento_location)
- self.assertEqual(404, r.status_code, "Memento was not gone")
+ self.checkResponse(TestConstants.OK, 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.assertEqual(2, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos")
-
- self.log("Create a memento at the deleted datetime")
+ @Test
+ def makeVersionsInsideASecond(self):
headers = {
- 'Content-Type': TestConstants.JSONLD_MIMETYPE,
- 'Prefer': TestConstants.PUT_PREFER_LENIENT,
- 'Memento-Datetime': new_date
+ 'Link': self.make_type(TestConstants.LDP_BASIC)
}
- 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")
+ 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)
- self.log("Check the memento exists again")
- r = self.do_get(memento_location)
- self.assertEqual(200, r.status_code, "Memento was not found")
+ version_endpoint = location + "/" + TestConstants.FCR_VERSIONS
+ r = self.do_get(version_endpoint)
+ self.checkResponse(TestConstants.OK, r)
+ self.checkMementoCount(1, version_endpoint)
- self.log("Passed")
+ time.sleep(1)
+
+ 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 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):
- self.log("Running doBinaryVersioningTest")
-
+ """ 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'
@@ -141,38 +194,55 @@ 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(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.assertEqual(200, r.status_code, "Did not find versions where we should")
+ self.checkResponse(TestConstants.OK, r)
+
+ self.log("Get description version endpoint")
+ 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.assertEqual(201, r.status_code, 'Did not create a version')
+ self.checkResponse(TestConstants.CREATED, r)
+
+ 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)
+
+ 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(TestConstants.CREATED, r)
+
+ 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("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.assertEqual(201, r.status_code, "Did not create version with specific date and body")
- 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.log("Check memento exists")
- r = self.do_head(memento_location)
- self.assertEqual(200, r.status_code, "Memento was not found")
- 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')}
@@ -180,52 +250,227 @@ 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(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.assertEqual(405, r.status_code, "Did not get denied PUT request.")
+ self.checkResponse(TestConstants.METHOD_NOT_ALLOWED, 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")
r = self.do_post(version_endpoint)
- self.assertEqual(201, r.status_code, 'Did not create a version')
+ self.checkResponse(TestConstants.CREATED, r)
self.log("Count mementos")
+ self.checkMementoCount(4, version_endpoint)
+
+ self.log("Try to DELETE the memento")
+ r = self.do_delete(memento_location)
+ self.checkResponse(TestConstants.METHOD_NOT_ALLOWED, r)
+
+ self.log("Check the memento exists again")
+ r = self.do_head(memento_location)
+ self.checkResponse(TestConstants.OK, r)
+
+ self.log("Validate count of mementos again")
+ self.checkMementoCount(4, version_endpoint)
+
+ @Test
+ def checkBinaryVersioning(self):
+ """ Test that changes to a binary are displayed on the binary description version endpoint too """
headers = {
- 'Accept': 'application/link-format'
+ 'Link': self.make_type(TestConstants.LDP_NON_RDF_SOURCE),
+ 'Content-Type': 'text/csv'
}
- r = self.do_get(version_endpoint, headers=headers)
- self.assertEqual(200, r.status_code, "Did not get the fcr:versions content")
- self.assertEqual(3, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos")
+ files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\n')}
- self.log("Delete a Memento")
- r = self.do_delete(memento_location)
- self.assertEqual(204, r.status_code, "Did not delete the Memento")
+ 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)
- self.log("Validate delete")
- r = self.do_get(memento_location)
- self.assertEqual(404, r.status_code, "Memento was not gone")
+ 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(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 from existing")
+ r = self.do_post(binary_versions)
+ self.checkResponse(201, 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.assertEqual(2, FedoraVersionTests.count_mementos(r), "Incorrect number of Mementos")
+ self.log("Count Mementos of binary")
+ self.checkMementoCount(2, binary_versions)
- self.log("Create a memento at the deleted datetime")
+ self.log("Count Mementos of binary description")
+ self.checkMementoCount(2, metadata_versions)
+
+ 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")
+ self.checkMementoCount(3, binary_versions)
+
+ self.log("Count Mementos of binary description")
+ self.checkMementoCount(3, metadata_versions)
+
+ @Test
+ def createBinaryVersionsWithTimeOrBody(self):
+ """ Test we can no longer provide a version datetime or a version body """
headers = {
- 'Content-Type': TestConstants.JSONLD_MIMETYPE,
- 'Prefer': TestConstants.PUT_PREFER_LENIENT,
- 'Memento-Datetime': new_date
+ '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)
+
+ r = self.do_get(description_location)
+ 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
+
+ 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')}
+ headers = {
+ 'Content-Type': 'text/csv',
+ 'Memento-Datetime': 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.assertEqual(201, r.status_code, "Did not create version with specific date and body")
+ self.checkResponse(TestConstants.BAD_REQUEST, 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.log("Check we haven't made a memento of the binary")
+ self.checkMementoCount(1, 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,
+ 'Memento-Datetime': the_date,
+ 'Prefer': TestConstants.PUT_PREFER_LENIENT
+ }
- self.log("Passed")
+ 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(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 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)
+
+ self.verifyGet(uri)
+
+ 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)
+
+ r = self.do_delete(uri)
+ self.checkResponse(TestConstants.NO_CONTENT, r)
+
+ self.verifyGone(uri)
+ # 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):
+ """ Test checking past versions of binary descriptions """
+ 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)