diff --git a/indexd/app.py b/indexd/app.py index a4278fa5..dacc3542 100644 --- a/indexd/app.py +++ b/indexd/app.py @@ -3,8 +3,10 @@ from .index.blueprint import blueprint as indexd_index_blueprint from .alias.blueprint import blueprint as indexd_alias_blueprint from .dos.blueprint import blueprint as indexd_dos_blueprint +from .drs.blueprint import blueprint as indexd_drs_blueprint from .blueprint import blueprint as cross_blueprint +from indexd.fence_client import FenceClient from indexd.urls.blueprint import blueprint as index_urls_blueprint import os @@ -19,10 +21,16 @@ def app_init(app, settings=None): from .default_settings import settings app.config.update(settings["config"]) app.auth = settings["auth"] + app.fence_client = FenceClient( + url=os.environ.get("PRESIGNED_FENCE_URL") + or "http://presigned-url-fence-service" + ) + app.hostname = os.environ.get("HOSTNAME") or "http://example.io" app.register_blueprint(indexd_bulk_blueprint) app.register_blueprint(indexd_index_blueprint) app.register_blueprint(indexd_alias_blueprint) app.register_blueprint(indexd_dos_blueprint) + app.register_blueprint(indexd_drs_blueprint) app.register_blueprint(cross_blueprint) app.register_blueprint(index_urls_blueprint, url_prefix="/_query/urls") diff --git a/indexd/auth/__init__.py b/indexd/auth/__init__.py index ad3565b9..1eac9eba 100644 --- a/indexd/auth/__init__.py +++ b/indexd/auth/__init__.py @@ -16,7 +16,7 @@ def authorize(*p): not present, or fallback to the previous check. """ if len(p) == 1: - f, = p + (f,) = p @wraps(f) def check_auth(*args, **kwargs): diff --git a/indexd/default_settings.py b/indexd/default_settings.py index 57289a56..3eed3fdb 100644 --- a/indexd/default_settings.py +++ b/indexd/default_settings.py @@ -32,6 +32,7 @@ ) } + CONFIG["DIST"] = [ { "name": "Other IndexD", @@ -46,6 +47,12 @@ "hints": [], "type": "dos", }, + { + "name": "DRS System", + "host": "https://example.com/api/ga4gh/drs/v1/", + "hints": [], + "type": "drs", + }, ] AUTH = SQLAlchemyAuthDriver("sqlite:///auth.sq3") diff --git a/indexd/drs/__init__.py b/indexd/drs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/indexd/drs/blueprint.py b/indexd/drs/blueprint.py new file mode 100644 index 00000000..cc9a7171 --- /dev/null +++ b/indexd/drs/blueprint.py @@ -0,0 +1,140 @@ +import flask +from indexd.errors import AuthError +from indexd.errors import UserError +from indexd.index.errors import NoRecordFound as IndexNoRecordFound +from indexd.errors import UnexpectedError +from indexd.index.blueprint import get_index + +blueprint = flask.Blueprint("drs", __name__) + +blueprint.config = dict() +blueprint.index_driver = None + + +@blueprint.route("/ga4gh/drs/v1/objects/", methods=["GET"]) +def get_drs_object(object_id): + """ + Returns a specific DRSobject with object_id + """ + ret = blueprint.index_driver.get(object_id) + + return flask.jsonify(indexd_to_drs(ret)), 200 + + +@blueprint.route("/ga4gh/drs/v1/objects", methods=["GET"]) +def list_drs_records(): + records = get_index()[0].json["records"] + ret = {"drs_objects": [indexd_to_drs(record, True) for record in records]} + + return flask.jsonify(ret), 200 + + +@blueprint.route( + "/ga4gh/drs/v1/objects//access", + defaults={"access_id": None}, + methods=["GET"], +) +@blueprint.route( + "/ga4gh/drs/v1/objects//access/", methods=["GET"] +) +def get_signed_url(object_id, access_id): + if not access_id: + raise (UserError("Access ID/Protocol is required.")) + res = flask.current_app.fence_client.get_signed_url_for_object( + object_id=object_id, access_id=access_id + ) + if not res: + raise IndexNoRecordFound("No signed url found") + + return res, 200 + + +def indexd_to_drs(record, list_drs=False): + bearer_token = flask.request.headers.get("AUTHORIZATION") + self_uri = "drs://" + flask.current_app.hostname + "/" + record["did"] + drs_object = { + "id": record["did"], + "description": "", + "mime_type": "application/json", + "name": record["file_name"], + "created_time": record["created_date"], + "updated_time": record["updated_date"], + "size": record["size"], + "aliases": [], + "contents": [], + "self_uri": self_uri, + "version": record["rev"], + } + + if "description" in record: + drs_object["description"] = record["description"] + if "alias" in record: + drs_object["aliases"].append(record["alias"]) + + if "contents" in record: + drs_object["contents"] = record["contents"] + + # access_methods mapping + if "urls" in record: + drs_object["access_methods"] = [] + for location in record["urls"]: + location_type = location.split(":")[ + 0 + ] # (s3, gs, ftp, gsiftp, globus, htsget, https, file) + + drs_object["access_methods"].append( + { + "type": location_type, + "access_url": flask.current_app.fence_client.get_signed_url_for_object( + record["did"], "" + ) + if bearer_token and not list_drs + else {"url": location}, + "access_id": location_type, + "region": "", + } + ) + print(drs_object) + + # parse out checksums + drs_object["checksums"] = [] + for k in record["hashes"]: + drs_object["checksums"].append({"checksum": record["hashes"][k], "type": k}) + + return drs_object + + +@blueprint.errorhandler(UserError) +def handle_user_error(err): + ret = {"msg": str(err), "status_code": 400} + return flask.jsonify(ret), 400 + + +@blueprint.errorhandler(AuthError) +def handle_auth_error(err): + ret = {"msg": str(err), "status_code": 401} + return flask.jsonify(ret), 401 + + +@blueprint.errorhandler(AuthError) +def handle_requester_auth_error(err): + ret = {"msg": str(err), "status_code": 403} + return flask.jsonify(ret), 403 + + +@blueprint.errorhandler(IndexNoRecordFound) +def handle_no_index_record_error(err): + ret = {"msg": str(err), "status_code": 404} + return flask.jsonify(ret), 404 + + +@blueprint.errorhandler(UnexpectedError) +def handle_unexpected_error(err): + ret = {"msg": str(err), "status_code": 500} + return flask.jsonify(ret), 500 + + +@blueprint.record +def get_config(setup_state): + index_config = setup_state.app.config["INDEX"] + blueprint.index_driver = index_config["driver"] diff --git a/indexd/errors.py b/indexd/errors.py index fdd26500..a5f1f6ea 100644 --- a/indexd/errors.py +++ b/indexd/errors.py @@ -11,3 +11,9 @@ class ConfigurationError(Exception): """ Configuration error. """ + + +class UnexpectedError(Exception): + """ + Unexpected Error + """ diff --git a/indexd/fence_client.py b/indexd/fence_client.py new file mode 100644 index 00000000..acac623f --- /dev/null +++ b/indexd/fence_client.py @@ -0,0 +1,45 @@ +from cdislogging import get_logger + +import flask +import requests + + +from indexd.index.errors import NoRecordFound as IndexNoRecordFound +from indexd.errors import UnexpectedError +from indexd.auth.errors import AuthError + + +logger = get_logger(__name__) + + +class FenceClient(object): + def __init__(self, url): + self.url = url + + def get_signed_url_for_object(self, object_id, access_id): + fence_server = self.url + api_url = fence_server.rstrip("/") + "/data/download/" + url = api_url + object_id + headers = flask.request.headers + if "AUTHORIZATION" not in headers: + logger.error("Bearer Token not available.") + raise AuthError("Not Authorized. Access Token Required.") + if access_id: + url += "?protocol=" + access_id + try: + req = requests.get(url, headers=headers) + except Exception as e: + logger.error("failed to reach fence at {0}: {1}".format(url + object_id, e)) + raise UnexpectedError("Failed to retrieve access url") + if req.status_code == 404: + logger.error( + "Not found. Fence could not find {}: {} with access id: {}".format( + url + object_id, req.text, access_id + ) + ) + raise IndexNoRecordFound( + "No document with id:{} with access_id:{}".format(object_id, access_id) + ) + elif req.status_code != 200: + raise UnexpectedError(req.text) + return req.json() diff --git a/openapis/swagger.yaml b/openapis/swagger.yaml index 5b56707d..dc753c17 100644 --- a/openapis/swagger.yaml +++ b/openapis/swagger.yaml @@ -37,6 +37,8 @@ tags: url: 'https://github.com/uc-cdis/indexd' - name: DOS description: 'Data Object Service Retrieval Endpoints' + - name: DRS + description: 'Data Repository Service Retrieval Endpoints' - name: system description: System endpoints schemes: @@ -783,7 +785,7 @@ paths: description: successful operation schema: $ref: '#/definitions/SystemStatsOutputRef' - /ga4gh/dos/v1/dataobjects: + '/ga4gh/dos/v1/dataobjects': get: summary: List the Data Objects operationId: ListDataObjects @@ -855,7 +857,7 @@ paths: description: |- The continuation token, which is used to page through large result sets. To get the next page of results, set this parameter to the value of - `next_page_token` from the previous response. + next_page_token from the previous response. tags: - DOS '/ga4gh/dos/v1/dataobjects/{GUID}': @@ -894,6 +896,154 @@ paths: type: string tags: - DOS + '/ga4gh/drs/v1/objects': + get: + summary: List all DrsObject. + description: 'Url field here contains their location and not the presigned url.' + operationId: ListDrsObject + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/ListDrsObject' + security: [] + tags: + - DRS + '/ga4gh/drs/v1/objects/{object_id}': + get: + summary: Get info about a DrsObject. + description: >- + Returns object metadata, and a list of access methods that can be used to fetch object bytes. + Returns presigned url given an access token in the url field, else returns file location. + operationId: GetObject + responses: + '200': + description: The DrsObject was found successfully. + schema: + $ref: '#/definitions/DrsObject' + # '202': + # description: > + # The operation is delayed and will continue asynchronously. + # The client should retry this same request after the delay specified by Retry-After header. + # headers: + # Retry-After: + # description: > + # Delay in seconds. The client should retry this same request after waiting for this duration. + # To simplify client response processing, this must be an integral relative time in seconds. + # This value SHOULD represent the minimum duration the client should wait before attempting + # the operation again with a reasonable expectation of success. When it is not feasible + # for the server to determine the actual expected delay, the server may return a + # brief, fixed value instead. + # type: integer + # format: int64 + '400': + description: The request is malformed. + schema: + $ref: '#/definitions/Error' + '401': + description: The request is unauthorized. + schema: + $ref: '#/definitions/Error' + '403': + description: The requester is not authorized to perform this action. + schema: + $ref: '#/definitions/Error' + '404': + description: The requested DrsObject wasn't found + schema: + $ref: '#/definitions/Error' + '500': + description: An unexpected error occurred. + schema: + $ref: '#/definitions/Error' + parameters: + - name: object_id + in: path + required: true + type: string + - in: query + name: expand + type: boolean + default: false + description: >- + NOT IMPLEMENTED YET. + + If false and the object_id refers to a bundle, then the ContentsObject array + contains only those objects directly contained in the bundle. That is, if the + bundle contains other bundles, those other bundles are not recursively + included in the result. + + If true and the object_id refers to a bundle, then the entire set of objects + in the bundle is expanded. That is, if the bundle contains aother bundles, + then those other bundles are recursively expanded and included in the result. + Recursion continues through the entire sub-tree of the bundle. + If the object_id refers to a blob, then the query parameter is ignored. + x-swagger-router-controller: ga4gh.drs.server + tags: + - DRS + '/ga4gh/drs/v1/objects/{object_id}/access/{access_id}': + get: + summary: Get a URL for fetching bytes. + description: >- + Returns a URL that can be used to fetch the bytes of a DrsObject. + This method only needs to be called when using an AccessMethod that contains an access_id + (e.g., for servers that use signed URLs for fetching object bytes). + operationId: GetAccessURL + responses: + '200': + description: The access URL was found successfully. + schema: + $ref: '#/definitions/AccessURL' + # '202': + # description: > + # The operation is delayed and will continue asynchronously. + # The client should retry this same request after the delay specified by Retry-After header. + # headers: + # Retry-After: + # description: > + # Delay in seconds. The client should retry this same request after waiting for this duration. + # To simplify client response processing, this must be an integral relative time in seconds. + # This value SHOULD represent the minimum duration the client should wait before attempting + # the operation again with a reasonable expectation of success. When it is not feasible + # for the server to determine the actual expected delay, the server may return a + # brief, fixed value instead. + # type: integer + # format: int64 + '400': + description: The request is malformed. + schema: + $ref: '#/definitions/Error' + '401': + description: The request is unauthorized. + schema: + $ref: '#/definitions/Error' + '404': + description: The requested access URL wasn't found + schema: + $ref: '#/definitions/Error' + '403': + description: The requester is not authorized to perform this action. + schema: + $ref: '#/definitions/Error' + '500': + description: An unexpected error occurred. + schema: + $ref: '#/definitions/Error' + parameters: + - name: object_id + in: path + required: true + type: string + description: An id of a DrsObject + - name: access_id + in: path + required: true + type: string + description: An access_id from the access_methods list of a DrsObject + tags: + - DRS + x-swagger-router-controller: ga4gh.drs.server + '/bulk/documents': post: tags: @@ -917,6 +1067,7 @@ paths: '400': description: Invalid status value security: [] + '/_query/urls/q': get: tags: @@ -1464,7 +1615,7 @@ definitions: description: |- The continuation token, which is used to page through large result sets. To get the next page of results, set this parameter to the value of - `next_page_token` from the previous response. + next_page_token from the previous response. description: |- Allows a requester to list and filter Data Objects. Only Data Objects matching all of the requested parameters will be returned. @@ -1603,6 +1754,205 @@ definitions: description: number of aliases to return hashes: $ref: '#/definitions/HashInfo' + DrsObject: + type: object + required: ['id', 'self_uri', 'size', 'created_time', 'checksums'] + properties: + id: + type: string + description: |- + An identifier unique to this DrsObject. + name: + type: string + description: |- + A string that can be used to name a DrsObject. + This string is made up of uppercase and lowercase letters, decimal digits, hypen, period, and underscore [A-Za-z0-9.-_]. See http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282[portable filenames]. + self_uri: + type: string + description: |- + A drs:// URI, as defined in the DRS documentation, that tells clients how to access this object. + The intent of this field is to make DRS objects self-contained, and therefore easier for clients to store and pass around. + example: + drs://drs.example.org/314159 + size: + type: integer + format: int64 + description: |- + For blobs, the blob size in bytes. + For bundles, the cumulative size, in bytes, of items in the contents field. + created_time: + type: string + format: date-time + description: |- + Timestamp of content creation in RFC3339. + (This is the creation time of the underlying content, not of the JSON object.) + updated_time: + type: string + format: date-time + description: >- + Timestamp of content update in RFC3339, identical to created_time in systems + that do not support updates. + (This is the update time of the underlying content, not of the JSON object.) + version: + type: string + description: >- + A string representing a version. + (Some systems may use checksum, a RFC3339 timestamp, or an incrementing version number.) + mime_type: + type: string + description: |- + A string providing the mime-type of the DrsObject. + example: + application/json + checksums: + type: array + minItems: 1 + items: + $ref: '#/definitions/Checksum' + description: >- + The checksum of the DrsObject. At least one checksum must be provided. + For blobs, the checksum is computed over the bytes in the blob. + For bundles, the checksum is computed over a sorted concatenation of the + checksums of its top-level contained objects (not recursive, names not included). + The list of checksums is sorted alphabetically (hex-code) before concatenation + and a further checksum is performed on the concatenated checksum value. + For example, if a bundle contains blobs with the following checksums: + md5(blob1) = 72794b6d + md5(blob2) = 5e089d29 + Then the checksum of the bundle is: + md5( concat( sort( md5(blob1), md5(blob2) ) ) ) + = md5( concat( sort( 72794b6d, 5e089d29 ) ) ) + = md5( concat( 5e089d29, 72794b6d ) ) + = md5( 5e089d2972794b6d ) + = f7a29a04 + access_methods: + type: array + minItems: 1 + items: + $ref: '#/definitions/AccessMethod' + description: |- + The list of access methods that can be used to fetch the DrsObject. + Required for single blobs; optional for bundles. + contents: + type: array + description: >- + If not set, this DrsObject is a single blob. + If set, this DrsObject is a bundle containing the listed ContentsObject s (some of which may be further nested). + items: + $ref: '#/definitions/ContentsObject' + description: + type: string + description: |- + A human readable description of the DrsObject. + aliases: + type: array + items: + type: string + description: >- + A list of strings that can be used to find other metadata + about this DrsObject from external metadata sources. These + aliases can be used to represent secondary + accession numbers or external GUIDs. + AccessURL: + type: object + required: ['url'] + properties: + url: + type: string + description: A fully resolvable URL that can be used to fetch the actual object bytes. + AccessMethod: + type: object + required: + - type + properties: + type: + type: string + enum: + - s3 + - gs + - ftp + - gsiftp + - globus + - htsget + - https + - file + description: >- + Type of the access method. + access_url: + $ref: '#/definitions/AccessURL' + description: >- + An AccessURL that can be used to fetch the actual object bytes. + Note that at least one of access_url and access_id must be provided. + access_id: + type: string + description: >- + An arbitrary string to be passed to the /access method to get an AccessURL. + This string must be unique within the scope of a single object. + Note that at least one of access_url and access_id must be provided. + region: + type: string + description: >- + Name of the region in the cloud service provider that the object belongs to. + example: + us-east-1 + ContentsObject: + type: object + properties: + name: + type: string + description: >- + A name declared by the bundle author that must be + used when materialising this object, + overriding any name directly associated with the object itself. + The name must be unique with the containing bundle. + This string is made up of uppercase and lowercase letters, decimal digits, hypen, period, and underscore [A-Za-z0-9.-_]. See http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282[portable filenames]. + id: + type: string + description: >- + A DRS identifier of a DrsObject (either a single blob or a nested bundle). + If this ContentsObject is an object within a nested bundle, then the id is + optional. Otherwise, the id is required. + drs_uri: + type: array + description: >- + A list of full DRS identifier URI paths + that may be used to obtain the object. + These URIs may be external to this DRS instance. + example: + drs://drs.example.org/314159 + items: + type: string + contents: + type: array + description: >- + If this ContentsObject describes a nested bundle and the caller specified + "?expand=true" on the request, then this contents array must be present and + describe the objects within the nested bundle. + items: + example: + {} + + required: + - name + Error: + description: + An object that can optionally include information about the error. + type: object + properties: + msg: + type: string + description: A detailed error message. + status_code: + type: integer + description: The integer representing the HTTP status code (e.g. 200, 404). + ListDrsObject: + type: object + properties: + drs_objects: + type: array + items: + $ref: '#/definitions/DrsObject' + ListRecords: type: object properties: diff --git a/test-requirements.txt b/test-requirements.txt index ec22659b..dddc8c1b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,4 +6,5 @@ pytest-cov==2.5.1 pytest-flask==0.8.1 PyYAML==5.1 swagger_spec_validator +responses -e git+https://github.com/uc-cdis/cdisutils-test.git@1.0.0#egg=cdisutilstest diff --git a/tests/default_test_settings.py b/tests/default_test_settings.py index 6f3ef066..4579d671 100644 --- a/tests/default_test_settings.py +++ b/tests/default_test_settings.py @@ -1,5 +1,6 @@ from indexd.default_settings import * from indexd.index.drivers.alchemy import SQLAlchemyIndexDriver +import os # override the default settings for INDEX because we want to test # both PREPEND_PREFIX and ADD_PREFIX_ALIAS, which should not both @@ -17,6 +18,8 @@ ) } +os.environ["PRESIGNED_FENCE_URL"] = "https://fictitious-commons.io/" +os.environ["HOSTNAME"] = "fictitious-commons.io" settings = {"config": CONFIG, "auth": AUTH} settings["config"]["TEST_DB"] = "postgres://postgres@localhost/test_migration_db" diff --git a/tests/test_drs.py b/tests/test_drs.py new file mode 100644 index 00000000..caccbb31 --- /dev/null +++ b/tests/test_drs.py @@ -0,0 +1,172 @@ +import json +import tests.conftest +import requests +import responses +from tests.default_test_settings import settings + + +def generate_presigned_url_response(did, protocol="", status=200): + full_url = ( + "https://fictitious-commons.io/data/download/" + did + "?protocol=" + protocol + ) + presigned_url = { + "url": "https://storage.googleapis.com/nih-mock-project-released-phs123-c2/RootStudyConsentSet_phs000007.Whatever.v666.p1.c2.FBI-BMW-CIA.tar.gz?GoogleAccessId=internal-someuser-1399@dcpstage-210518.iam.gserviceaccount.com&Expires=1582215120&Signature=hUsgjkegdsfkjbsajkafnsdjksdnfjknbdsajkfbsdkjfbjdfbkjdasfbnjsdnfjsnd2FTr%2FKs2kGKs0fJ8v5elFk5NQAYdrGcU3kROrzJuHUbI%2BMZ839SAbAz2rbMBuC9e46%2BdB91%2FA==&userProject=dcf-mock-project" + } + responses.add(responses.GET, full_url, json=presigned_url, status=status) + return presigned_url + + +def get_doc(has_version=True, urls=list(), drs_list=0): + doc = { + "form": "object", + "size": 123, + "urls": ["s3://endpointurl/bucket/key"], + "hashes": {"md5": "8b9942cf415384b27cadf1f4d2d682e5"}, + } + if has_version: + doc["version"] = "1" + if urls: + doc["urls"] = urls + # if drs_list > 0: + # ret = {"drs_objects": []} + # for _ in range(drs_list): + # ret["drs_objects"].append(doc) + # return ret + return doc + + +def test_drs_get(client, user): + data = get_doc() + res_1 = client.post("/index/", json=data, headers=user) + assert res_1.status_code == 200 + rec_1 = res_1.json + res_2 = client.get("/ga4gh/drs/v1/objects/" + rec_1["did"]) + + assert res_2.status_code == 200 + rec_2 = res_2.json + assert rec_2["id"] == rec_1["did"] + assert rec_2["size"] == data["size"] + for k in data["hashes"]: + assert rec_2["checksums"][0]["checksum"] == data["hashes"][k] + assert rec_2["checksums"][0]["type"] == k + assert rec_2["version"] + assert rec_2["self_uri"] == "drs://fictitious-commons.io/" + rec_1["did"] + + +def test_drs_multiple_endpointurl(client, user): + object_urls = { + "sftp": "sftp://endpointurl/bucket/key", + "ftp": "ftp://endpointurl/bucket/key", + "gs": "gs://endpointurl/bucket/key", + "s3": "s3://endpointurl/bucket/key", + } + data = get_doc(urls=list(object_urls.values())) + res_1 = client.post("/index/", json=data, headers=user) + assert res_1.status_code == 200 + rec_1 = res_1.json + res_2 = client.get("/ga4gh/drs/v1/objects/" + rec_1["did"]) + + assert res_2.status_code == 200 + rec_2 = res_2.json + assert rec_2["id"] == rec_1["did"] + + for url in rec_2["access_methods"]: + protocol = url["type"] + assert url["access_url"]["url"] == object_urls[protocol] + + +@responses.activate +def test_drs_get_with_presigned_url(client, user): + data = get_doc() + res_1 = client.post("/index/", json=data, headers=user) + assert res_1.status_code == 200 + rec_1 = res_1.json + presigned = generate_presigned_url_response(rec_1["did"]) + res_2 = client.get( + "/ga4gh/drs/v1/objects/" + rec_1["did"], headers={"AUTHORIZATION": "12345"} + ) + assert res_2.status_code == 200 + rec_2 = res_2.json + assert rec_2["id"] == rec_1["did"] + assert rec_2["size"] == data["size"] + for k in data["hashes"]: + assert rec_2["checksums"][0]["checksum"] == data["hashes"][k] + assert rec_2["checksums"][0]["type"] == k + + assert rec_2["access_methods"][0]["access_url"] == presigned + + +def test_drs_list(client, user): + record_length = 7 + data = get_doc() + submitted_guids = [] + for _ in range(record_length): + res_1 = client.post("/index/", json=data, headers=user) + submitted_guids.append(res_1.json["did"]) + assert res_1.status_code == 200 + res_2 = client.get("/ga4gh/drs/v1/objects") + assert res_2.status_code == 200 + rec_2 = res_2.json + assert len(rec_2["drs_objects"]) == record_length + assert submitted_guids.sort() == [r["id"] for r in rec_2["drs_objects"]].sort() + + +def test_get_drs_record_not_found(client, user): + # test exception raised at nonexistent + fake_did = "testprefix:d96bab16-c4e1-44ac-923a-04328b6fe78f" + res = client.get("/ga4gh/drs/v1/objects/" + fake_did) + assert res.status_code == 404 + + +@responses.activate +def test_get_presigned_url_with_access_id(client, user): + data = get_doc() + res_1 = client.post("/index/", json=data, headers=user) + assert res_1.status_code == 200 + rec_1 = res_1.json + access_id_list = ["s3", "gs", "ftp"] + for access_id in access_id_list: + presigned = generate_presigned_url_response(rec_1["did"], access_id) + res_2 = client.get( + "/ga4gh/drs/v1/objects/" + rec_1["did"] + "/access/" + access_id, + headers={"AUTHORIZATION": "12345"}, + ) + assert res_2.status_code == 200 + assert res_2.json == presigned + + +def test_get_presigned_url_no_access_id(client, user): + data = get_doc() + res_1 = client.post("/index/", json=data, headers=user) + assert res_1.status_code == 200 + rec_1 = res_1.json + generate_presigned_url_response(rec_1["did"], "s3") + res_2 = client.get( + "/ga4gh/drs/v1/objects/" + rec_1["did"] + "/access/", + headers={"AUTHORIZATION": "12345"}, + ) + assert res_2.status_code == 400 + + +def test_get_presigned_url_no_bearer_token(client, user): + data = get_doc() + res_1 = client.post("/index/", json=data, headers=user) + assert res_1.status_code == 200 + rec_1 = res_1.json + generate_presigned_url_response(rec_1["did"], "s3") + res_2 = client.get("/ga4gh/drs/v1/objects/" + rec_1["did"] + "/access/s3") + assert res_2.status_code == 403 + + +@responses.activate +def test_get_presigned_url_wrong_access_id(client, user): + data = get_doc() + res_1 = client.post("/index/", json=data, headers=user) + assert res_1.status_code == 200 + rec_1 = res_1.json + generate_presigned_url_response(rec_1["did"], "s2", status=404) + res_2 = client.get( + "/ga4gh/drs/v1/objects/" + rec_1["did"] + "/access/s2", + headers={"AUTHORIZATION": "12345"}, + ) + assert res_2.status_code == 404