diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1750d39 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: test + +on: + push: + branches: + - wip/testing + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - ubuntu-22.04 + - ubuntu-20.04 + - windows-latest + - windows-2022 + - windows-2019 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + architecture: x64 + + - name: Install Python dependencies + run: pip install -r requirements.txt + + - name: Run Python Tests + run: python -m unittest discover diff --git a/.gitignore b/.gitignore index 9c92902..e7af935 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Typical virtualenv dir /venv/ +/.venv/ # IDE settings /.idea/ diff --git a/config.ini.default b/config.ini.default index 1d957d6..96a5de4 100644 --- a/config.ini.default +++ b/config.ini.default @@ -6,4 +6,4 @@ port=8125 fallback_semantic_matching_service=https://example.org/semantic_matching_service eclass_semantic_matching_service=https://example.org/semantic_matching_service cdd_semantic_matching_service=https://example.org/semantic_matching_service -debug_semantic_matching_service_endpoints=../debug_endpoints.json \ No newline at end of file +debug_semantic_matching_service_endpoints=debug_endpoints.json diff --git a/requirements.txt b/requirements.txt index 33bfc16..b4d8e99 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/semantic_id_resolver/resolver.py b/semantic_id_resolver/resolver.py index cac45f5..bb07c8d 100644 --- a/semantic_id_resolver/resolver.py +++ b/semantic_id_resolver/resolver.py @@ -1,9 +1,11 @@ import enum +from pathlib import Path from typing import Optional, Dict from urllib.parse import urlparse import json import dns.resolver +from parser.irdi_parser import IRDIParser class DebugSemanticMatchingServiceEndpoints: @@ -23,7 +25,9 @@ def __init__(self, debug_endpoints: Dict[str, str]): @classmethod def from_file(cls, filename: str) -> "DebugSemanticMatchingServiceEndpoints": - with open(filename, "r") as file: + base_dir = str(Path(__file__).resolve().parent.parent) + resource_path = base_dir + "/" + filename + with open(resource_path, "r") as file: debug_endpoints = json.load(file) return DebugSemanticMatchingServiceEndpoints(debug_endpoints) @@ -33,18 +37,20 @@ def get_debug_endpoint(self, semantic_id: str) -> Optional[str]: def is_iri_not_irdi(semantic_id: str) -> Optional[bool]: """ - :return: `True`, if `semantic_id` is a IRI, False if it is an `IRDI`, None for neither + :return: `True`, if `semantic_id` is an IRI, False if it is an IRDI, None for neither """ + # Check IRDI + try: + IRDIParser().parse(semantic_id) + return False + except ValueError: + pass + # Check IRI parsed_url = urlparse(semantic_id) - # Check if the scheme is present, which indicates it's a URI if parsed_url.scheme: return True - # Check if there is a colon in the netloc, which could indicate an IRI - elif ':' in parsed_url.netloc: - return False - # If neither condition is met, return None - else: - return None + # Not IRDI or IRI + return None class IRDISources(enum.Enum): @@ -103,9 +109,10 @@ def find_semantic_matching_service(self, semantic_id: str) -> Optional[str]: return debug_endpoint # Check for IRI and IRDI - if is_iri_not_irdi(semantic_id) is True: + is_iri = is_iri_not_irdi(semantic_id) + if is_iri is True: return _iri_find_semantic_matching_service(semantic_id) - elif is_iri_not_irdi(semantic_id) is False: + elif is_iri is False: return self._irdi_find_semantic_matching_service(semantic_id) else: return None diff --git a/semantic_id_resolver/service.py b/semantic_id_resolver/service.py index 0b57130..07625bd 100644 --- a/semantic_id_resolver/service.py +++ b/semantic_id_resolver/service.py @@ -57,7 +57,7 @@ def get_semantic_matching_service( endpoint = found_endpoint return SMSResponse( semantic_matching_service_endpoint=endpoint, - meta_information={} # Todo + meta_information={} ) @@ -82,7 +82,7 @@ def get_semantic_matching_service( DEBUG_ENDPOINTS = resolver.DebugSemanticMatchingServiceEndpoints.from_file( config["RESOLVER"]["debug_semantic_matching_service_endpoints"] ) - print(f"USING DEBUG ENDPOINTS FROM {config["RESOLVER"]["debug_semantic_matching_service_endpoints"]}") + print(f"USING DEBUG ENDPOINTS FROM {config['RESOLVER']['debug_semantic_matching_service_endpoints']}") except FileNotFoundError: DEBUG_ENDPOINTS = resolver.DebugSemanticMatchingServiceEndpoints(debug_endpoints={}) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_resolving_service.py b/test/test_resolving_service.py new file mode 100644 index 0000000..eba6220 --- /dev/null +++ b/test/test_resolving_service.py @@ -0,0 +1,145 @@ +import os +import configparser +from typing import Dict +import multiprocessing + +import dns +import requests +import unittest + +from fastapi import FastAPI +import uvicorn + +from semantic_id_resolver import resolver +from semantic_id_resolver.service import SemanticIdResolvingService, SMSRequest + +from contextlib import contextmanager +import signal +import time + + +def run_server(): + # Load test configuration + config = configparser.ConfigParser() + config.read([ + os.path.abspath(os.path.join(os.path.dirname(__file__), "../test_resources/config.ini")), + ]) + + # Define test configuration + IRDI_MATCHER_DICT: Dict[resolver.IRDISources, str] = { + resolver.IRDISources.ECLASS: config["RESOLVER"]["eclass_semantic_matching_service"], + resolver.IRDISources.IEC_CDD: config["RESOLVER"]["cdd_semantic_matching_service"] + } + + try: + DEBUG_ENDPOINTS = resolver.DebugSemanticMatchingServiceEndpoints.from_file( + config["RESOLVER"]["debug_semantic_matching_service_endpoints"] + ) + print(f"USING DEBUG ENDPOINTS FROM {config['RESOLVER']['debug_semantic_matching_service_endpoints']}") + except FileNotFoundError: + DEBUG_ENDPOINTS = resolver.DebugSemanticMatchingServiceEndpoints(debug_endpoints={}) + + # Mock SemanticIdResolvingService for testing + mock_resolver = resolver.SemanticIdResolver(IRDI_MATCHER_DICT, DEBUG_ENDPOINTS) + semantic_id_resolver_service = SemanticIdResolvingService( + endpoint=config["SERVICE"]["endpoint"], + fallback_semantic_matching_service_endpoint=config["RESOLVER"]["fallback_semantic_matching_service"], + semantic_id_resolver=mock_resolver + ) + + # Mock TXT record + class MockTXTRecord: + def __init__(self, strings): + self.strings = strings + + # Mock DNS resolving manually, mock for unittest does not work in this context + def mock_dns_resolver_resolve(qname, rdtype): + return [MockTXTRecord([b"semantic_matcher: https://example.org/iri_semantic_matching_service"])] + dns.resolver.resolve = mock_dns_resolver_resolve + + # Run server + app = FastAPI() + app.include_router(semantic_id_resolver_service.router) + uvicorn.run(app, host=str(config["SERVICE"]["ENDPOINT"]), port=int(config["SERVICE"]["PORT"]), log_level="error") + + +@contextmanager +def run_server_context(): + server_process = multiprocessing.Process(target=run_server) + server_process.start() + try: + time.sleep(2) # Wait for the server to start + yield + finally: + server_process.terminate() + server_process.join(timeout=5) + if server_process.is_alive(): + os.kill(server_process.pid, signal.SIGKILL) + server_process.join() + + +class TestSemanticMatchingService(unittest.TestCase): + + def test_semantic_matching_service_iri(self): + with run_server_context(): + sms_request = SMSRequest(semantic_id="foo://example.org:1234/over/there?name=bar#page=3") + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/iri_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) + + def test_semantic_matching_service_irdi_eclass(self): + with run_server_context(): + sms_request = SMSRequest(semantic_id="0173-0001#01-ACK323#7") + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/eclass_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) + + def test_semantic_matching_service_irdi_cdd(self): + with run_server_context(): + sms_request = SMSRequest(semantic_id="0112-0001#01-ACK323#7") + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/cdd_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) + + def test_semantic_matching_service_fallback(self): + with run_server_context(): + sms_request = SMSRequest(semantic_id="nothing") + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/fallback_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) + + def test_semantic_matching_service_debug(self): + with run_server_context(): + sms_request = SMSRequest(semantic_id="https://example.org/semanticIDone") + response = requests.get( + "http://localhost:8125/get_semantic_matching_service", + data=sms_request.model_dump_json() + ) + self.assertEqual( + "https://example.org/debug_semantic_matching_service", + response.json()["semantic_matching_service_endpoint"] + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/test_resources/config.ini b/test_resources/config.ini new file mode 100644 index 0000000..d546395 --- /dev/null +++ b/test_resources/config.ini @@ -0,0 +1,9 @@ +[SERVICE] +endpoint=127.0.0.1 +port=8125 + +[RESOLVER] +fallback_semantic_matching_service=https://example.org/fallback_semantic_matching_service +eclass_semantic_matching_service=https://example.org/eclass_semantic_matching_service +cdd_semantic_matching_service=https://example.org/cdd_semantic_matching_service +debug_semantic_matching_service_endpoints=test_resources/debug_endpoints.json diff --git a/test_resources/debug_endpoints.json b/test_resources/debug_endpoints.json new file mode 100644 index 0000000..85573cb --- /dev/null +++ b/test_resources/debug_endpoints.json @@ -0,0 +1,3 @@ +{ + "https://example.org/semanticIDone": "https://example.org/debug_semantic_matching_service" +}