Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions app/ro_crates/routes/post_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from apiflask import APIBlueprint, Schema
from apiflask.fields import String, Boolean
from marshmallow.fields import Nested
from flask import Response
from flask import Response, current_app

from app.services.validation_service import (
queue_ro_crate_validation_task,
Expand Down Expand Up @@ -81,7 +81,10 @@ def validate_ro_crate_via_id(json_data, crate_id) -> tuple[Response, int]:
else:
profile_name = None

return queue_ro_crate_validation_task(minio_config, crate_id, root_path, profile_name, webhook_url)
profiles_path = current_app.config["PROFILES_PATH"]

return queue_ro_crate_validation_task(minio_config, crate_id, root_path, profile_name,
webhook_url, profiles_path)


@post_routes_bp.post("/validate_metadata")
Expand Down
6 changes: 4 additions & 2 deletions app/services/validation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@


def queue_ro_crate_validation_task(
minio_config, crate_id, root_path=None, profile_name=None, webhook_url=None
minio_config, crate_id, root_path=None, profile_name=None, webhook_url=None,
profiles_path=None
) -> tuple[Response, int]:
"""
Queues an RO-Crate for validation with Celery.
Expand All @@ -51,7 +52,8 @@ def queue_ro_crate_validation_task(
raise InvalidAPIUsage(f"No RO-Crate with prefix: {crate_id}", 400)

try:
process_validation_task_by_id.delay(minio_config, crate_id, root_path, profile_name, webhook_url)
process_validation_task_by_id.delay(minio_config, crate_id, root_path,
profile_name, webhook_url, profiles_path)
return jsonify({"message": "Validation in progress"}), 202

except Exception as e:
Expand Down
11 changes: 7 additions & 4 deletions app/tasks/validation_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@

@celery.task
def process_validation_task_by_id(
minio_config: dict, crate_id: str, root_path: str, profile_name: str | None, webhook_url: str | None
minio_config: dict, crate_id: str, root_path: str, profile_name: str | None,
webhook_url: str | None, profiles_path: str | None
) -> None:
"""
Background task to process the RO-Crate validation by ID.
Expand All @@ -56,7 +57,7 @@ def process_validation_task_by_id(
logging.info(f"Processing validation task for {file_path}")

# Perform validation:
validation_result = perform_ro_crate_validation(file_path, profile_name)
validation_result = perform_ro_crate_validation(file_path, profile_name, profiles_path=profiles_path)

if isinstance(validation_result, str):
logging.error(f"Validation failed: {validation_result}")
Expand Down Expand Up @@ -158,14 +159,15 @@ def process_validation_task_by_metadata(


def perform_ro_crate_validation(
file_path: str, profile_name: str | None, skip_checks_list: Optional[list] = None
file_path: str, profile_name: str | None, skip_checks_list: Optional[list] = None, profiles_path: Optional[str] = None
) -> ValidationResult | str:
"""
Validates an RO-Crate using the provided file path and profile name.

:param file_path: The path to the RO-Crate file to validate
:param profile_name: The name of the validation profile to use. Defaults to None. If None, the CRS4 validator will
attempt to determine the profile.
:param profiles_path: The path to the profiles definition directory
:param skip_checks_list: A list of checks to skip, if needed
:return: The validation result.
:raises Exception: If an error occurs during the validation process.
Expand All @@ -183,7 +185,8 @@ def perform_ro_crate_validation(
settings = services.ValidationSettings(
rocrate_uri=full_file_path,
**({"profile_identifier": profile_name} if profile_name else {}),
**({"skip_checks": skip_checks_list} if skip_checks_list else {})
**({"skip_checks": skip_checks_list} if skip_checks_list else {}),
**({"extra_profiles_path": profiles_path} if profiles_path else {})
)

return services.validate(settings)
Expand Down
31 changes: 16 additions & 15 deletions app/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,32 @@
from flask import Flask


def get_env(name: str, default=None, required=False):
value = os.environ.get(name, default)
if required and value is None:
raise RuntimeError(f"Missing required environment variable: {name}")
return value


class Config:
"""Base configuration class for the Flask application."""

SECRET_KEY = os.getenv("SECRET_KEY", "my_precious")

# Celery configuration:
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL")
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND")
CELERY_BROKER_URL = get_env("CELERY_BROKER_URL", required=False)
CELERY_RESULT_BACKEND = get_env("CELERY_RESULT_BACKEND", required=False)

# MinIO configuration:
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT")
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY")
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY")
MINIO_BUCKET_NAME = os.getenv("MINIO_BUCKET_NAME", "bucket-name")
# rocrate validator configuration:
PROFILES_PATH = get_env("PROFILES_PATH", required=False)


class DevelopmentConfig(Config):
"""Development configuration class."""

DEBUG = True
ENV = "development"


class ProductionConfig(Config):
"""Production configuration class."""

DEBUG = False
ENV = "production"


class InvalidAPIUsage(Exception):
Expand All @@ -63,10 +61,13 @@ def make_celery(app: Flask = None) -> Celery:
:param app: The Flask application to use.
:return: The Celery instance.
"""
env = os.environ.get("FLASK_ENV", "development")
config_cls = ProductionConfig if env == "production" else DevelopmentConfig

celery = Celery(
app.import_name if app else __name__,
broker=os.getenv("CELERY_BROKER_URL"),
backend=os.getenv("CELERY_RESULT_BACKEND"),
broker=config_cls.CELERY_BROKER_URL,
backend=config_cls.CELERY_RESULT_BACKEND,
)

if app:
Expand Down
3 changes: 3 additions & 0 deletions docker-compose-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ services:
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
- MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME}
- PROFILES_PATH=/app/profiles
depends_on:
- redis
- minio
volumes:
- ./tests/data/rocrate_validator_profiles:/app/profiles:ro

redis:
image: "redis:alpine"
Expand Down
31 changes: 21 additions & 10 deletions tests/test_api_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def client():
# Test POST API: /v1/ro_crates/{crate_id}/validation

@pytest.mark.parametrize(
"crate_id, payload, status_code, response_json",
"crate_id, payload, profiles_path, status_code, response_json",
[
(
"crate-123", {
Expand All @@ -27,7 +27,9 @@ def client():
"root_path": "base_path",
"webhook_url": "https://webhook.example.com",
"profile_name": "default"
}, 202, {"message": "Validation in progress"}
},
None,
202, {"message": "Validation in progress"}
),
(
"crate-123", {
Expand All @@ -38,9 +40,11 @@ def client():
"ssl": False,
"bucket": "test_bucket"
},
"root_path": "base_path",
"root_path": "base_path",
"webhook_url": "https://webhook.example.com",
}, 202, {"message": "Validation in progress"}
},
None,
202, {"message": "Validation in progress"}
),
(
"crate-123", {
Expand All @@ -51,9 +55,11 @@ def client():
"ssl": False,
"bucket": "test_bucket"
},
"root_path": "base_path",
"root_path": "base_path",
"profile_name": "default"
}, 202, {"message": "Validation in progress"}
},
None,
202, {"message": "Validation in progress"}
),
(
"crate-123", {
Expand All @@ -66,7 +72,9 @@ def client():
},
"webhook_url": "https://webhook.example.com",
"profile_name": "default"
}, 202, {"message": "Validation in progress"}
},
None,
202, {"message": "Validation in progress"}
),
(
"crate-123", {
Expand All @@ -77,14 +85,17 @@ def client():
"ssl": False,
"bucket": "test_bucket"
},
}, 202, {"message": "Validation in progress"}
},
None,
202, {"message": "Validation in progress"}
),
],
ids=["validate_by_id", "validate_with_missing_profile_name",
"validate_with_missing_webhook_url", "validate_with_missing_root_path",
"validate_with_missing_root_path_and_profile_name_and_webhook_url"]
)
def test_validate_by_id_success(client: FlaskClient, crate_id: str, payload: dict, status_code: int, response_json: dict):
def test_validate_by_id_success(client: FlaskClient, crate_id: str, payload: dict,
profiles_path: str, status_code: int, response_json: dict):
with patch("app.ro_crates.routes.post_routes.queue_ro_crate_validation_task") as mock_queue:
mock_queue.return_value = (response_json, status_code)

Expand All @@ -96,7 +107,7 @@ def test_validate_by_id_success(client: FlaskClient, crate_id: str, payload: dic
webhook_url = payload["webhook_url"] if "webhook_url" in payload else None
assert response.status_code == status_code
assert response.json == response_json
mock_queue.assert_called_once_with(minio_config, crate_id, root_path, profile_name, webhook_url)
mock_queue.assert_called_once_with(minio_config, crate_id, root_path, profile_name, webhook_url, profiles_path)


@pytest.mark.parametrize(
Expand Down
74 changes: 74 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,80 @@ def test_directory_rocrate_validation():
assert response_result["passed"] is False


def test_extra_profile_rocrate_validation():
ro_crate = "ro_crate_2"
profile_name = "alpha-crate-0.1"
url_post = f"http://localhost:5001/v1/ro_crates/{ro_crate}/validation"
url_get = f"http://localhost:5001/v1/ro_crates/{ro_crate}/validation"
headers = {
"accept": "application/json",
"Content-Type": "application/json"
}

# The API expects the JSON to be passed as a string
post_payload = {
"minio_config": {
"endpoint": "minio:9000",
"accesskey": "minioadmin",
"secret": "minioadmin",
"ssl": False,
"bucket": "ro-crates"
},
"profile_name": profile_name
}
get_payload = {
"minio_config": {
"endpoint": "minio:9000",
"accesskey": "minioadmin",
"secret": "minioadmin",
"ssl": False,
"bucket": "ro-crates"
}
}

# POST action and tests
response = requests.post(url_post, json=post_payload, headers=headers)
response_result = response.json()['message']

# Print response for debugging
print("Status Code:", response.status_code)
print("Response JSON:", response_result)

# Assertions
assert response.status_code == 202
assert response_result == "Validation in progress"

# wait for ro-crate to be validated
time.sleep(10)

# GET action and tests
response = requests.get(url_get, json=get_payload, headers=headers)
response_result = response.json()

# Print response for debugging
print("Status Code:", response.status_code)
print("Response JSON:", response_result)

start_time = time.time()
while response.status_code == 400:
time.sleep(10)
# GET action and tests
response = requests.get(url_get, json=get_payload, headers=headers)
response_result = response.json()
# Print response for debugging
print("Status Code:", response.status_code)
print("Response JSON:", response_result)

elapsed = time.time() - start_time
if elapsed > 60:
print("60 seconds passed. Exiting loop")
break

# Assertions
assert response.status_code == 200
assert response_result["passed"] is False


def test_ignore_rocrates_not_on_basepath():
ro_crate = "ro_crate_4"
url_post = f"http://localhost:5001/v1/ro_crates/{ro_crate}/validation"
Expand Down
17 changes: 11 additions & 6 deletions tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def flask_app():
# Test function: queue_ro_crate_validation_task

@pytest.mark.parametrize(
"crate_id, rocrate_exists, minio_client, delay_side_effects, payload, status_code, response_dict",
"crate_id, rocrate_exists, minio_client, delay_side_effects, payload, profiles_path, status_code, response_dict",
[
(
"crate123", True, "minio_client", None,
Expand All @@ -37,7 +37,9 @@ def flask_app():
"root_path": "base_path",
"webhook_url": "https://webhook.example.com",
"profile_name": "default"
}, 202, {"message": "Validation in progress"}
},
None,
202, {"message": "Validation in progress"}
),
(
"crate123", True, "minio_client", Exception("Celery down"),
Expand All @@ -52,7 +54,9 @@ def flask_app():
"root_path": "base_path",
"webhook_url": "https://webhook.example.com",
"profile_name": "default"
}, 500, {"error": "Celery down"}
},
None,
500, {"error": "Celery down"}
),
],
ids=["successful_queue", "celery_server_down"]
Expand All @@ -65,7 +69,7 @@ def test_queue_ro_crate_validation_task(
mock_exists,
mock_delay,
flask_app: FlaskClient, crate_id: str, rocrate_exists: bool, minio_client: str,
delay_side_effects: Exception, payload: dict, status_code: int, response_dict: dict
delay_side_effects: Exception, payload: dict, profiles_path: str, status_code: int, response_dict: dict
):
mock_delay.side_effect = delay_side_effects
mock_exists.return_value = rocrate_exists
Expand All @@ -76,11 +80,12 @@ def test_queue_ro_crate_validation_task(
profile_name = payload["profile_name"] if "profile_name" in payload else None
webhook_url = payload["webhook_url"] if "webhook_url" in payload else None

response, status_code = queue_ro_crate_validation_task(minio_config, crate_id, root_path, profile_name, webhook_url)
response, status_code = queue_ro_crate_validation_task(minio_config, crate_id, root_path,
profile_name, webhook_url, profiles_path)

mock_client.assert_called_once_with(minio_config)
mock_exists.assert_called_once_with(minio_client, minio_config["bucket"], crate_id, root_path)
mock_delay.assert_called_once_with(minio_config, crate_id, root_path, profile_name, webhook_url)
mock_delay.assert_called_once_with(minio_config, crate_id, root_path, profile_name, webhook_url, profiles_path)
assert status_code == status_code
assert response.json == response_dict

Expand Down
Loading