prefix-register 0.2.2

A PostgreSQL-backed namespace prefix registry for CURIE expansion and prefix management
Documentation
"""Tests for the prefix_register Python bindings."""

import os

import pytest
from prefix_register import PrefixRegistry

# Get database URL from environment
DATABASE_URL = os.environ.get(
    "DATABASE_URL", "postgres://postgres@localhost/prefix_test"
)


@pytest.fixture
async def registry():
    """Create a registry connected to the test database."""
    reg = await PrefixRegistry.new(DATABASE_URL, 5)
    yield reg


@pytest.fixture
async def clean_registry(registry):
    """Create a registry and clean up test data before and after."""
    # Note: In a real scenario, you'd want to clean up test data
    # This is a simplified version that just returns the registry
    yield registry


class TestPrefixRegistryBasic:
    """Basic tests for PrefixRegistry."""

    async def test_new_connection(self):
        """Test creating a new connection."""
        registry = await PrefixRegistry.new(DATABASE_URL, 5)
        assert registry is not None

    async def test_new_with_retry(self):
        """Test creating a connection with retry logic."""
        registry = await PrefixRegistry.new_with_retry(
            DATABASE_URL, 5, max_retries=3, initial_delay_ms=100, max_delay_ms=1000
        )
        assert registry is not None

    async def test_new_with_empty_url_fails(self):
        """Test that empty database URL raises ValueError."""
        with pytest.raises(ValueError, match="database_url cannot be empty"):
            await PrefixRegistry.new("", 5)

    async def test_new_with_zero_connections_fails(self):
        """Test that zero connections raises ValueError."""
        with pytest.raises(ValueError, match="max_connections must be greater than 0"):
            await PrefixRegistry.new(DATABASE_URL, 0)


class TestPrefixStorage:
    """Tests for prefix storage operations."""

    async def test_store_prefix_if_new(self, registry):
        """Test storing a new prefix."""
        # Use unique prefix/URI to avoid conflicts with other tests
        import uuid

        unique_id = str(uuid.uuid4())[:8]
        prefix = f"test_{unique_id}"
        uri = f"http://example.org/test/{unique_id}/"

        stored = await registry.store_prefix_if_new(prefix, uri)
        assert stored is True

        # Trying to store again with same URI should return False
        stored_again = await registry.store_prefix_if_new(f"other_{unique_id}", uri)
        assert stored_again is False

    async def test_store_prefixes_if_new_batch(self, registry):
        """Test batch storing prefixes."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]

        prefixes = [
            (f"batch1_{unique_id}", f"http://example.org/batch1/{unique_id}/"),
            (f"batch2_{unique_id}", f"http://example.org/batch2/{unique_id}/"),
            (f"batch3_{unique_id}", f"http://example.org/batch3/{unique_id}/"),
        ]

        result = await registry.store_prefixes_if_new(prefixes)
        assert result["stored"] == 3
        assert result["skipped"] == 0


class TestPrefixLookup:
    """Tests for prefix lookup operations."""

    async def test_get_uri_for_prefix(self, registry):
        """Test looking up URI by prefix."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]
        prefix = f"lookup_{unique_id}"
        uri = f"http://example.org/lookup/{unique_id}/"

        await registry.store_prefix_if_new(prefix, uri)

        found_uri = await registry.get_uri_for_prefix(prefix)
        assert found_uri == uri

    async def test_get_uri_for_unknown_prefix(self, registry):
        """Test looking up unknown prefix returns None."""
        result = await registry.get_uri_for_prefix(
            "definitely_not_a_real_prefix_xyz123"
        )
        assert result is None

    async def test_get_prefix_for_uri(self, registry):
        """Test looking up prefix by URI."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]
        prefix = f"reverse_{unique_id}"
        uri = f"http://example.org/reverse/{unique_id}/"

        await registry.store_prefix_if_new(prefix, uri)

        found_prefix = await registry.get_prefix_for_uri(uri)
        assert found_prefix == prefix

    async def test_get_prefix_for_unknown_uri(self, registry):
        """Test looking up unknown URI returns None."""
        result = await registry.get_prefix_for_uri(
            "http://definitely-not-registered.example.org/"
        )
        assert result is None


class TestCurieExpansion:
    """Tests for CURIE expansion."""

    async def test_expand_curie(self, registry):
        """Test expanding a CURIE."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]
        prefix = f"curie_{unique_id}"
        uri = f"http://example.org/curie/{unique_id}/"

        await registry.store_prefix_if_new(prefix, uri)

        expanded = await registry.expand_curie(prefix, "Person")
        assert expanded == f"{uri}Person"

    async def test_expand_curie_unknown_prefix(self, registry):
        """Test expanding CURIE with unknown prefix returns None."""
        result = await registry.expand_curie("unknown_prefix_xyz123", "Thing")
        assert result is None

    async def test_expand_curie_batch(self, registry):
        """Test batch expanding CURIEs."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]
        prefix = f"batch_exp_{unique_id}"
        uri = f"http://example.org/batch_exp/{unique_id}/"

        await registry.store_prefix_if_new(prefix, uri)

        curies = [
            (prefix, "Person"),
            ("unknown_xyz", "Thing"),
            (prefix, "Organization"),
        ]
        results = await registry.expand_curie_batch(curies)

        assert len(results) == 3
        assert results[0] == f"{uri}Person"
        assert results[1] is None  # Unknown prefix
        assert results[2] == f"{uri}Organization"


class TestPrefixEnumeration:
    """Tests for prefix enumeration."""

    async def test_get_all_prefixes(self, registry):
        """Test getting all registered prefixes."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]
        prefix = f"all_{unique_id}"
        uri = f"http://example.org/all/{unique_id}/"

        await registry.store_prefix_if_new(prefix, uri)

        all_prefixes = await registry.get_all_prefixes()
        assert isinstance(all_prefixes, dict)
        assert prefix in all_prefixes
        assert all_prefixes[prefix] == uri

    async def test_prefix_count(self, registry):
        """Test counting registered prefixes."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]

        initial_count = await registry.prefix_count()

        await registry.store_prefix_if_new(
            f"count_{unique_id}", f"http://example.org/count/{unique_id}/"
        )

        new_count = await registry.prefix_count()
        assert new_count == initial_count + 1


class TestUriShortening:
    """Tests for URI shortening operations."""

    async def test_shorten_uri(self, registry):
        """Test shortening a URI to prefix:local tuple."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]
        prefix = f"short_{unique_id}"
        uri = f"http://example.org/short/{unique_id}/"

        await registry.store_prefix_if_new(prefix, uri)

        result = await registry.shorten_uri(f"{uri}Person")
        assert result is not None
        assert result[0] == prefix
        assert result[1] == "Person"

    async def test_shorten_uri_unknown(self, registry):
        """Test shortening an unknown URI returns None."""
        result = await registry.shorten_uri(
            "http://totally-unknown-namespace.example.org/Thing"
        )
        assert result is None

    async def test_shorten_uri_longest_match(self, registry):
        """Test that longest namespace match wins."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]

        # Register two overlapping namespaces
        short_prefix = f"short_{unique_id}"
        short_uri = f"http://example.org/{unique_id}/"
        long_prefix = f"long_{unique_id}"
        long_uri = f"http://example.org/{unique_id}/sub#"

        await registry.store_prefix_if_new(short_prefix, short_uri)
        await registry.store_prefix_if_new(long_prefix, long_uri)

        # URI matching the longer namespace should use that prefix
        result = await registry.shorten_uri(f"{long_uri}Thing")
        assert result is not None
        assert result[0] == long_prefix
        assert result[1] == "Thing"

        # URI matching only the shorter namespace should use that prefix
        result2 = await registry.shorten_uri(f"{short_uri}Other")
        assert result2 is not None
        assert result2[0] == short_prefix
        assert result2[1] == "Other"

    async def test_shorten_uri_or_full(self, registry):
        """Test shorten_uri_or_full convenience method."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]
        prefix = f"orf_{unique_id}"
        uri = f"http://example.org/orf/{unique_id}/"

        await registry.store_prefix_if_new(prefix, uri)

        # Known namespace returns CURIE
        result = await registry.shorten_uri_or_full(f"{uri}Person")
        assert result == f"{prefix}:Person"

        # Unknown namespace returns original URI
        unknown_uri = "http://unknown-namespace.example.org/Thing"
        result2 = await registry.shorten_uri_or_full(unknown_uri)
        assert result2 == unknown_uri

    async def test_shorten_uri_batch(self, registry):
        """Test batch shortening URIs."""
        import uuid

        unique_id = str(uuid.uuid4())[:8]
        prefix = f"batch_{unique_id}"
        uri = f"http://example.org/batch/{unique_id}/"

        await registry.store_prefix_if_new(prefix, uri)

        uris = [
            f"{uri}Person",
            "http://unknown.example.org/Thing",
            f"{uri}Organization",
        ]
        results = await registry.shorten_uri_batch(uris)

        assert len(results) == 3
        assert results[0] == (prefix, "Person")
        assert results[1] is None  # Unknown namespace
        assert results[2] == (prefix, "Organization")