mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 08:26:21 +02:00
Merge 2.0 to master (#651)
This commit is contained in:
parent
3666ece2c5
commit
b9d7bf9a8b
212 changed files with 13940 additions and 6180 deletions
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from trustgraph.query.graph_embeddings.milvus.service import Processor
|
||||
from trustgraph.schema import Value, GraphEmbeddingsRequest
|
||||
from trustgraph.schema import Term, GraphEmbeddingsRequest, IRI, LITERAL
|
||||
|
||||
|
||||
class TestMilvusGraphEmbeddingsQueryProcessor:
|
||||
|
|
@ -68,50 +68,50 @@ class TestMilvusGraphEmbeddingsQueryProcessor:
|
|||
def test_create_value_with_http_uri(self, processor):
|
||||
"""Test create_value with HTTP URI"""
|
||||
result = processor.create_value("http://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "http://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "http://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_value_with_https_uri(self, processor):
|
||||
"""Test create_value with HTTPS URI"""
|
||||
result = processor.create_value("https://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "https://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "https://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_value_with_literal(self, processor):
|
||||
"""Test create_value with literal value"""
|
||||
result = processor.create_value("just a literal string")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "just a literal string"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_empty_string(self, processor):
|
||||
"""Test create_value with empty string"""
|
||||
result = processor.create_value("")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == ""
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_partial_uri(self, processor):
|
||||
"""Test create_value with string that looks like URI but isn't complete"""
|
||||
result = processor.create_value("http")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "http"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_ftp_uri(self, processor):
|
||||
"""Test create_value with FTP URI (should not be detected as URI)"""
|
||||
result = processor.create_value("ftp://example.com/file")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "ftp://example.com/file"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_graph_embeddings_single_vector(self, processor):
|
||||
|
|
@ -138,17 +138,17 @@ class TestMilvusGraphEmbeddingsQueryProcessor:
|
|||
[0.1, 0.2, 0.3], 'test_user', 'test_collection', limit=10
|
||||
)
|
||||
|
||||
# Verify results are converted to Value objects
|
||||
# Verify results are converted to Term objects
|
||||
assert len(result) == 3
|
||||
assert isinstance(result[0], Value)
|
||||
assert result[0].value == "http://example.com/entity1"
|
||||
assert result[0].is_uri is True
|
||||
assert isinstance(result[1], Value)
|
||||
assert result[1].value == "http://example.com/entity2"
|
||||
assert result[1].is_uri is True
|
||||
assert isinstance(result[2], Value)
|
||||
assert isinstance(result[0], Term)
|
||||
assert result[0].iri == "http://example.com/entity1"
|
||||
assert result[0].type == IRI
|
||||
assert isinstance(result[1], Term)
|
||||
assert result[1].iri == "http://example.com/entity2"
|
||||
assert result[1].type == IRI
|
||||
assert isinstance(result[2], Term)
|
||||
assert result[2].value == "literal entity"
|
||||
assert result[2].is_uri is False
|
||||
assert result[2].type == LITERAL
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_graph_embeddings_multiple_vectors(self, processor):
|
||||
|
|
@ -186,7 +186,7 @@ class TestMilvusGraphEmbeddingsQueryProcessor:
|
|||
|
||||
# Verify results are deduplicated and limited
|
||||
assert len(result) == 3
|
||||
entity_values = [r.value for r in result]
|
||||
entity_values = [r.iri if r.type == IRI else r.value for r in result]
|
||||
assert "http://example.com/entity1" in entity_values
|
||||
assert "http://example.com/entity2" in entity_values
|
||||
assert "http://example.com/entity3" in entity_values
|
||||
|
|
@ -246,7 +246,7 @@ class TestMilvusGraphEmbeddingsQueryProcessor:
|
|||
|
||||
# Verify duplicates are removed
|
||||
assert len(result) == 3
|
||||
entity_values = [r.value for r in result]
|
||||
entity_values = [r.iri if r.type == IRI else r.value for r in result]
|
||||
assert len(set(entity_values)) == 3 # All unique
|
||||
assert "http://example.com/entity1" in entity_values
|
||||
assert "http://example.com/entity2" in entity_values
|
||||
|
|
@ -346,14 +346,14 @@ class TestMilvusGraphEmbeddingsQueryProcessor:
|
|||
assert len(result) == 4
|
||||
|
||||
# Check URI entities
|
||||
uri_results = [r for r in result if r.is_uri]
|
||||
uri_results = [r for r in result if r.type == IRI]
|
||||
assert len(uri_results) == 2
|
||||
uri_values = [r.value for r in uri_results]
|
||||
uri_values = [r.iri for r in uri_results]
|
||||
assert "http://example.com/uri_entity" in uri_values
|
||||
assert "https://example.com/another_uri" in uri_values
|
||||
|
||||
# Check literal entities
|
||||
literal_results = [r for r in result if not r.is_uri]
|
||||
literal_results = [r for r in result if not r.type == IRI]
|
||||
assert len(literal_results) == 2
|
||||
literal_values = [r.value for r in literal_results]
|
||||
assert "literal entity text" in literal_values
|
||||
|
|
@ -486,7 +486,7 @@ class TestMilvusGraphEmbeddingsQueryProcessor:
|
|||
|
||||
# Verify results from all dimensions
|
||||
assert len(result) == 3
|
||||
entity_values = [r.value for r in result]
|
||||
entity_values = [r.iri if r.type == IRI else r.value for r in result]
|
||||
assert "entity_2d" in entity_values
|
||||
assert "entity_4d" in entity_values
|
||||
assert "entity_3d" in entity_values
|
||||
|
|
@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch
|
|||
pytest.skip("Pinecone library missing protoc_gen_openapiv2 dependency", allow_module_level=True)
|
||||
|
||||
from trustgraph.query.graph_embeddings.pinecone.service import Processor
|
||||
from trustgraph.schema import Value
|
||||
from trustgraph.schema import Term, IRI, LITERAL
|
||||
|
||||
|
||||
class TestPineconeGraphEmbeddingsQueryProcessor:
|
||||
|
|
@ -105,27 +105,27 @@ class TestPineconeGraphEmbeddingsQueryProcessor:
|
|||
uri_entity = "http://example.org/entity"
|
||||
value = processor.create_value(uri_entity)
|
||||
|
||||
assert isinstance(value, Value)
|
||||
assert isinstance(value, Term)
|
||||
assert value.value == uri_entity
|
||||
assert value.is_uri == True
|
||||
assert value.type == IRI
|
||||
|
||||
def test_create_value_https_uri(self, processor):
|
||||
"""Test create_value method for HTTPS URI entities"""
|
||||
uri_entity = "https://example.org/entity"
|
||||
value = processor.create_value(uri_entity)
|
||||
|
||||
assert isinstance(value, Value)
|
||||
assert isinstance(value, Term)
|
||||
assert value.value == uri_entity
|
||||
assert value.is_uri == True
|
||||
assert value.type == IRI
|
||||
|
||||
def test_create_value_literal(self, processor):
|
||||
"""Test create_value method for literal entities"""
|
||||
literal_entity = "literal_entity"
|
||||
value = processor.create_value(literal_entity)
|
||||
|
||||
assert isinstance(value, Value)
|
||||
assert isinstance(value, Term)
|
||||
assert value.value == literal_entity
|
||||
assert value.is_uri == False
|
||||
assert value.type == LITERAL
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_graph_embeddings_single_vector(self, processor):
|
||||
|
|
@ -165,11 +165,11 @@ class TestPineconeGraphEmbeddingsQueryProcessor:
|
|||
# Verify results
|
||||
assert len(entities) == 3
|
||||
assert entities[0].value == 'http://example.org/entity1'
|
||||
assert entities[0].is_uri == True
|
||||
assert entities[0].type == IRI
|
||||
assert entities[1].value == 'entity2'
|
||||
assert entities[1].is_uri == False
|
||||
assert entities[1].type == LITERAL
|
||||
assert entities[2].value == 'http://example.org/entity3'
|
||||
assert entities[2].is_uri == True
|
||||
assert entities[2].type == IRI
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_graph_embeddings_multiple_vectors(self, processor, mock_query_message):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from unittest import IsolatedAsyncioTestCase
|
|||
|
||||
# Import the service under test
|
||||
from trustgraph.query.graph_embeddings.qdrant.service import Processor
|
||||
from trustgraph.schema import IRI, LITERAL
|
||||
|
||||
|
||||
class TestQdrantGraphEmbeddingsQuery(IsolatedAsyncioTestCase):
|
||||
|
|
@ -85,10 +86,10 @@ class TestQdrantGraphEmbeddingsQuery(IsolatedAsyncioTestCase):
|
|||
value = processor.create_value('http://example.com/entity')
|
||||
|
||||
# Assert
|
||||
assert hasattr(value, 'value')
|
||||
assert value.value == 'http://example.com/entity'
|
||||
assert hasattr(value, 'is_uri')
|
||||
assert value.is_uri == True
|
||||
assert hasattr(value, 'iri')
|
||||
assert value.iri == 'http://example.com/entity'
|
||||
assert hasattr(value, 'type')
|
||||
assert value.type == IRI
|
||||
|
||||
@patch('trustgraph.query.graph_embeddings.qdrant.service.QdrantClient')
|
||||
@patch('trustgraph.base.GraphEmbeddingsQueryService.__init__')
|
||||
|
|
@ -109,10 +110,10 @@ class TestQdrantGraphEmbeddingsQuery(IsolatedAsyncioTestCase):
|
|||
value = processor.create_value('https://secure.example.com/entity')
|
||||
|
||||
# Assert
|
||||
assert hasattr(value, 'value')
|
||||
assert value.value == 'https://secure.example.com/entity'
|
||||
assert hasattr(value, 'is_uri')
|
||||
assert value.is_uri == True
|
||||
assert hasattr(value, 'iri')
|
||||
assert value.iri == 'https://secure.example.com/entity'
|
||||
assert hasattr(value, 'type')
|
||||
assert value.type == IRI
|
||||
|
||||
@patch('trustgraph.query.graph_embeddings.qdrant.service.QdrantClient')
|
||||
@patch('trustgraph.base.GraphEmbeddingsQueryService.__init__')
|
||||
|
|
@ -135,8 +136,8 @@ class TestQdrantGraphEmbeddingsQuery(IsolatedAsyncioTestCase):
|
|||
# Assert
|
||||
assert hasattr(value, 'value')
|
||||
assert value.value == 'regular entity name'
|
||||
assert hasattr(value, 'is_uri')
|
||||
assert value.is_uri == False
|
||||
assert hasattr(value, 'type')
|
||||
assert value.type == LITERAL
|
||||
|
||||
@patch('trustgraph.query.graph_embeddings.qdrant.service.QdrantClient')
|
||||
@patch('trustgraph.base.GraphEmbeddingsQueryService.__init__')
|
||||
|
|
@ -428,14 +429,14 @@ class TestQdrantGraphEmbeddingsQuery(IsolatedAsyncioTestCase):
|
|||
assert len(result) == 3
|
||||
|
||||
# Check URI entities
|
||||
uri_entities = [entity for entity in result if hasattr(entity, 'is_uri') and entity.is_uri]
|
||||
uri_entities = [entity for entity in result if entity.type == IRI]
|
||||
assert len(uri_entities) == 2
|
||||
uri_values = [entity.value for entity in uri_entities]
|
||||
uri_values = [entity.iri for entity in uri_entities]
|
||||
assert 'http://example.com/entity1' in uri_values
|
||||
assert 'https://secure.example.com/entity2' in uri_values
|
||||
|
||||
|
||||
# Check regular entities
|
||||
regular_entities = [entity for entity in result if hasattr(entity, 'is_uri') and not entity.is_uri]
|
||||
regular_entities = [entity for entity in result if entity.type == LITERAL]
|
||||
assert len(regular_entities) == 1
|
||||
assert regular_entities[0].value == 'regular entity'
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from trustgraph.query.triples.memgraph.service import Processor
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
|
||||
class TestMemgraphQueryUserCollectionIsolation:
|
||||
|
|
@ -24,9 +24,9 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
p=Value(value="http://example.com/p", is_uri=True),
|
||||
o=Value(value="test_object", is_uri=False),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=Term(type=IRI, iri="http://example.com/p"),
|
||||
o=Term(type=LITERAL, value="test_object"),
|
||||
limit=1000
|
||||
)
|
||||
|
||||
|
|
@ -65,8 +65,8 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
p=Value(value="http://example.com/p", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=Term(type=IRI, iri="http://example.com/p"),
|
||||
o=None,
|
||||
limit=1000
|
||||
)
|
||||
|
|
@ -105,9 +105,9 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=None,
|
||||
o=Value(value="http://example.com/o", is_uri=True),
|
||||
o=Term(type=IRI, iri="http://example.com/o"),
|
||||
limit=1000
|
||||
)
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=1000
|
||||
|
|
@ -185,8 +185,8 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=None,
|
||||
p=Value(value="http://example.com/p", is_uri=True),
|
||||
o=Value(value="literal", is_uri=False),
|
||||
p=Term(type=IRI, iri="http://example.com/p"),
|
||||
o=Term(type=LITERAL, value="literal"),
|
||||
limit=1000
|
||||
)
|
||||
|
||||
|
|
@ -225,7 +225,7 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=None,
|
||||
p=Value(value="http://example.com/p", is_uri=True),
|
||||
p=Term(type=IRI, iri="http://example.com/p"),
|
||||
o=None,
|
||||
limit=1000
|
||||
)
|
||||
|
|
@ -265,7 +265,7 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
collection="test_collection",
|
||||
s=None,
|
||||
p=None,
|
||||
o=Value(value="test_value", is_uri=False),
|
||||
o=Term(type=LITERAL, value="test_value"),
|
||||
limit=1000
|
||||
)
|
||||
|
||||
|
|
@ -355,7 +355,7 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
|
||||
# Query without user/collection fields
|
||||
query = TriplesQueryRequest(
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=1000
|
||||
|
|
@ -385,7 +385,7 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=1000
|
||||
|
|
@ -416,17 +416,17 @@ class TestMemgraphQueryUserCollectionIsolation:
|
|||
assert len(result) == 2
|
||||
|
||||
# First triple (literal object)
|
||||
assert result[0].s.value == "http://example.com/s"
|
||||
assert result[0].s.is_uri == True
|
||||
assert result[0].p.value == "http://example.com/p1"
|
||||
assert result[0].p.is_uri == True
|
||||
assert result[0].s.iri == "http://example.com/s"
|
||||
assert result[0].s.type == IRI
|
||||
assert result[0].p.iri == "http://example.com/p1"
|
||||
assert result[0].p.type == IRI
|
||||
assert result[0].o.value == "literal_value"
|
||||
assert result[0].o.is_uri == False
|
||||
|
||||
assert result[0].o.type == LITERAL
|
||||
|
||||
# Second triple (URI object)
|
||||
assert result[1].s.value == "http://example.com/s"
|
||||
assert result[1].s.is_uri == True
|
||||
assert result[1].p.value == "http://example.com/p2"
|
||||
assert result[1].p.is_uri == True
|
||||
assert result[1].o.value == "http://example.com/o"
|
||||
assert result[1].o.is_uri == True
|
||||
assert result[1].s.iri == "http://example.com/s"
|
||||
assert result[1].s.type == IRI
|
||||
assert result[1].p.iri == "http://example.com/p2"
|
||||
assert result[1].p.type == IRI
|
||||
assert result[1].o.iri == "http://example.com/o"
|
||||
assert result[1].o.type == IRI
|
||||
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from trustgraph.query.triples.neo4j.service import Processor
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
|
||||
class TestNeo4jQueryUserCollectionIsolation:
|
||||
|
|
@ -24,21 +24,23 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
p=Value(value="http://example.com/p", is_uri=True),
|
||||
o=Value(value="test_object", is_uri=False)
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=Term(type=IRI, iri="http://example.com/p"),
|
||||
o=Term(type=LITERAL, value="test_object"),
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.return_value = ([], MagicMock(), MagicMock())
|
||||
|
||||
|
||||
await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify SPO query for literal includes user/collection
|
||||
expected_query = (
|
||||
"MATCH (src:Node {uri: $src, user: $user, collection: $collection})-"
|
||||
"[rel:Rel {uri: $rel, user: $user, collection: $collection}]->"
|
||||
"(dest:Literal {value: $value, user: $user, collection: $collection}) "
|
||||
"RETURN $src as src"
|
||||
"RETURN $src as src "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
|
|
@ -63,23 +65,25 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
p=Value(value="http://example.com/p", is_uri=True),
|
||||
o=None
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=Term(type=IRI, iri="http://example.com/p"),
|
||||
o=None,
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.return_value = ([], MagicMock(), MagicMock())
|
||||
|
||||
|
||||
await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify SP query for literals includes user/collection
|
||||
expected_literal_query = (
|
||||
"MATCH (src:Node {uri: $src, user: $user, collection: $collection})-"
|
||||
"[rel:Rel {uri: $rel, user: $user, collection: $collection}]->"
|
||||
"(dest:Literal {user: $user, collection: $collection}) "
|
||||
"RETURN dest.value as dest"
|
||||
"RETURN dest.value as dest "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
expected_literal_query,
|
||||
src="http://example.com/s",
|
||||
|
|
@ -88,13 +92,14 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
collection="test_collection",
|
||||
database_='neo4j'
|
||||
)
|
||||
|
||||
|
||||
# Verify SP query for nodes includes user/collection
|
||||
expected_node_query = (
|
||||
"MATCH (src:Node {uri: $src, user: $user, collection: $collection})-"
|
||||
"[rel:Rel {uri: $rel, user: $user, collection: $collection}]->"
|
||||
"(dest:Node {user: $user, collection: $collection}) "
|
||||
"RETURN dest.uri as dest"
|
||||
"RETURN dest.uri as dest "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
|
|
@ -118,21 +123,23 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=None,
|
||||
o=Value(value="http://example.com/o", is_uri=True)
|
||||
o=Term(type=IRI, iri="http://example.com/o"),
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.return_value = ([], MagicMock(), MagicMock())
|
||||
|
||||
|
||||
await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify SO query for nodes includes user/collection
|
||||
expected_query = (
|
||||
"MATCH (src:Node {uri: $src, user: $user, collection: $collection})-"
|
||||
"[rel:Rel {user: $user, collection: $collection}]->"
|
||||
"(dest:Node {uri: $uri, user: $user, collection: $collection}) "
|
||||
"RETURN rel.uri as rel"
|
||||
"RETURN rel.uri as rel "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
|
|
@ -156,23 +163,25 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=None,
|
||||
o=None
|
||||
o=None,
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.return_value = ([], MagicMock(), MagicMock())
|
||||
|
||||
|
||||
await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify S query includes user/collection
|
||||
expected_query = (
|
||||
"MATCH (src:Node {uri: $src, user: $user, collection: $collection})-"
|
||||
"[rel:Rel {user: $user, collection: $collection}]->"
|
||||
"(dest:Literal {user: $user, collection: $collection}) "
|
||||
"RETURN rel.uri as rel, dest.value as dest"
|
||||
"RETURN rel.uri as rel, dest.value as dest "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
expected_query,
|
||||
src="http://example.com/s",
|
||||
|
|
@ -194,20 +203,22 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=None,
|
||||
p=Value(value="http://example.com/p", is_uri=True),
|
||||
o=Value(value="literal", is_uri=False)
|
||||
p=Term(type=IRI, iri="http://example.com/p"),
|
||||
o=Term(type=LITERAL, value="literal"),
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.return_value = ([], MagicMock(), MagicMock())
|
||||
|
||||
|
||||
await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify PO query for literals includes user/collection
|
||||
expected_query = (
|
||||
"MATCH (src:Node {user: $user, collection: $collection})-"
|
||||
"[rel:Rel {uri: $uri, user: $user, collection: $collection}]->"
|
||||
"(dest:Literal {value: $value, user: $user, collection: $collection}) "
|
||||
"RETURN src.uri as src"
|
||||
"RETURN src.uri as src "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
|
|
@ -232,20 +243,22 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=None,
|
||||
p=Value(value="http://example.com/p", is_uri=True),
|
||||
o=None
|
||||
p=Term(type=IRI, iri="http://example.com/p"),
|
||||
o=None,
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.return_value = ([], MagicMock(), MagicMock())
|
||||
|
||||
|
||||
await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify P query includes user/collection
|
||||
expected_query = (
|
||||
"MATCH (src:Node {user: $user, collection: $collection})-"
|
||||
"[rel:Rel {uri: $uri, user: $user, collection: $collection}]->"
|
||||
"(dest:Literal {user: $user, collection: $collection}) "
|
||||
"RETURN src.uri as src, dest.value as dest"
|
||||
"RETURN src.uri as src, dest.value as dest "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
|
|
@ -270,19 +283,21 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
collection="test_collection",
|
||||
s=None,
|
||||
p=None,
|
||||
o=Value(value="test_value", is_uri=False)
|
||||
o=Term(type=LITERAL, value="test_value"),
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.return_value = ([], MagicMock(), MagicMock())
|
||||
|
||||
|
||||
await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify O query for literals includes user/collection
|
||||
expected_query = (
|
||||
"MATCH (src:Node {user: $user, collection: $collection})-"
|
||||
"[rel:Rel {user: $user, collection: $collection}]->"
|
||||
"(dest:Literal {value: $value, user: $user, collection: $collection}) "
|
||||
"RETURN src.uri as src, rel.uri as rel"
|
||||
"RETURN src.uri as src, rel.uri as rel "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
|
|
@ -307,34 +322,37 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
collection="test_collection",
|
||||
s=None,
|
||||
p=None,
|
||||
o=None
|
||||
o=None,
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.return_value = ([], MagicMock(), MagicMock())
|
||||
|
||||
|
||||
await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify wildcard query for literals includes user/collection
|
||||
expected_literal_query = (
|
||||
"MATCH (src:Node {user: $user, collection: $collection})-"
|
||||
"[rel:Rel {user: $user, collection: $collection}]->"
|
||||
"(dest:Literal {user: $user, collection: $collection}) "
|
||||
"RETURN src.uri as src, rel.uri as rel, dest.value as dest"
|
||||
"RETURN src.uri as src, rel.uri as rel, dest.value as dest "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
expected_literal_query,
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
database_='neo4j'
|
||||
)
|
||||
|
||||
|
||||
# Verify wildcard query for nodes includes user/collection
|
||||
expected_node_query = (
|
||||
"MATCH (src:Node {user: $user, collection: $collection})-"
|
||||
"[rel:Rel {user: $user, collection: $collection}]->"
|
||||
"(dest:Node {user: $user, collection: $collection}) "
|
||||
"RETURN src.uri as src, rel.uri as rel, dest.uri as dest"
|
||||
"RETURN src.uri as src, rel.uri as rel, dest.uri as dest "
|
||||
"LIMIT 10"
|
||||
)
|
||||
|
||||
mock_driver.execute_query.assert_any_call(
|
||||
|
|
@ -355,9 +373,10 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
|
||||
# Query without user/collection fields
|
||||
query = TriplesQueryRequest(
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=None,
|
||||
o=None
|
||||
o=None,
|
||||
limit=10
|
||||
)
|
||||
|
||||
mock_driver.execute_query.return_value = ([], MagicMock(), MagicMock())
|
||||
|
|
@ -384,47 +403,48 @@ class TestNeo4jQueryUserCollectionIsolation:
|
|||
query = TriplesQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
s=Value(value="http://example.com/s", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/s"),
|
||||
p=None,
|
||||
o=None
|
||||
o=None,
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
# Mock some results
|
||||
mock_record1 = MagicMock()
|
||||
mock_record1.data.return_value = {
|
||||
"rel": "http://example.com/p1",
|
||||
"dest": "literal_value"
|
||||
}
|
||||
|
||||
|
||||
mock_record2 = MagicMock()
|
||||
mock_record2.data.return_value = {
|
||||
"rel": "http://example.com/p2",
|
||||
"dest": "http://example.com/o"
|
||||
}
|
||||
|
||||
|
||||
# Return results for literal query, empty for node query
|
||||
mock_driver.execute_query.side_effect = [
|
||||
([mock_record1], MagicMock(), MagicMock()), # Literal query
|
||||
([mock_record2], MagicMock(), MagicMock()) # Node query
|
||||
]
|
||||
|
||||
|
||||
result = await processor.query_triples(query)
|
||||
|
||||
# Verify results are proper Triple objects
|
||||
assert len(result) == 2
|
||||
|
||||
# First triple (literal object)
|
||||
assert result[0].s.value == "http://example.com/s"
|
||||
assert result[0].s.is_uri == True
|
||||
assert result[0].p.value == "http://example.com/p1"
|
||||
assert result[0].p.is_uri == True
|
||||
assert result[0].s.iri == "http://example.com/s"
|
||||
assert result[0].s.type == IRI
|
||||
assert result[0].p.iri == "http://example.com/p1"
|
||||
assert result[0].p.type == IRI
|
||||
assert result[0].o.value == "literal_value"
|
||||
assert result[0].o.is_uri == False
|
||||
|
||||
assert result[0].o.type == LITERAL
|
||||
|
||||
# Second triple (URI object)
|
||||
assert result[1].s.value == "http://example.com/s"
|
||||
assert result[1].s.is_uri == True
|
||||
assert result[1].p.value == "http://example.com/p2"
|
||||
assert result[1].p.is_uri == True
|
||||
assert result[1].o.value == "http://example.com/o"
|
||||
assert result[1].o.is_uri == True
|
||||
assert result[1].s.iri == "http://example.com/s"
|
||||
assert result[1].s.type == IRI
|
||||
assert result[1].p.iri == "http://example.com/p2"
|
||||
assert result[1].p.type == IRI
|
||||
assert result[1].o.iri == "http://example.com/o"
|
||||
assert result[1].o.type == IRI
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
"""
|
||||
Unit tests for Cassandra Objects GraphQL Query Processor
|
||||
Unit tests for Cassandra Rows GraphQL Query Processor (Unified Table Implementation)
|
||||
|
||||
Tests the business logic of the GraphQL query processor including:
|
||||
- GraphQL schema generation from RowSchema
|
||||
- Query execution and validation
|
||||
- CQL translation logic
|
||||
- Schema configuration handling
|
||||
- Query execution using unified rows table
|
||||
- Name sanitization
|
||||
- GraphQL query execution
|
||||
- Message processing logic
|
||||
"""
|
||||
|
||||
|
|
@ -12,119 +13,91 @@ import pytest
|
|||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
import json
|
||||
|
||||
import strawberry
|
||||
from strawberry import Schema
|
||||
|
||||
from trustgraph.query.objects.cassandra.service import Processor
|
||||
from trustgraph.schema import ObjectsQueryRequest, ObjectsQueryResponse, GraphQLError
|
||||
from trustgraph.query.rows.cassandra.service import Processor
|
||||
from trustgraph.schema import RowsQueryRequest, RowsQueryResponse, GraphQLError
|
||||
from trustgraph.schema import RowSchema, Field
|
||||
|
||||
|
||||
class TestObjectsGraphQLQueryLogic:
|
||||
"""Test business logic without external dependencies"""
|
||||
|
||||
def test_get_python_type_mapping(self):
|
||||
"""Test schema field type conversion to Python types"""
|
||||
processor = MagicMock()
|
||||
processor.get_python_type = Processor.get_python_type.__get__(processor, Processor)
|
||||
|
||||
# Basic type mappings
|
||||
assert processor.get_python_type("string") == str
|
||||
assert processor.get_python_type("integer") == int
|
||||
assert processor.get_python_type("float") == float
|
||||
assert processor.get_python_type("boolean") == bool
|
||||
assert processor.get_python_type("timestamp") == str
|
||||
assert processor.get_python_type("date") == str
|
||||
assert processor.get_python_type("time") == str
|
||||
assert processor.get_python_type("uuid") == str
|
||||
|
||||
# Unknown type defaults to str
|
||||
assert processor.get_python_type("unknown_type") == str
|
||||
|
||||
def test_create_graphql_type_basic_fields(self):
|
||||
"""Test GraphQL type creation for basic field types"""
|
||||
processor = MagicMock()
|
||||
processor.get_python_type = Processor.get_python_type.__get__(processor, Processor)
|
||||
processor.create_graphql_type = Processor.create_graphql_type.__get__(processor, Processor)
|
||||
|
||||
# Create test schema
|
||||
schema = RowSchema(
|
||||
name="test_table",
|
||||
description="Test table",
|
||||
fields=[
|
||||
Field(
|
||||
name="id",
|
||||
type="string",
|
||||
primary=True,
|
||||
required=True,
|
||||
description="Primary key"
|
||||
),
|
||||
Field(
|
||||
name="name",
|
||||
type="string",
|
||||
required=True,
|
||||
description="Name field"
|
||||
),
|
||||
Field(
|
||||
name="age",
|
||||
type="integer",
|
||||
required=False,
|
||||
description="Optional age"
|
||||
),
|
||||
Field(
|
||||
name="active",
|
||||
type="boolean",
|
||||
required=False,
|
||||
description="Status flag"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Create GraphQL type
|
||||
graphql_type = processor.create_graphql_type("test_table", schema)
|
||||
|
||||
# Verify type was created
|
||||
assert graphql_type is not None
|
||||
assert hasattr(graphql_type, '__name__')
|
||||
assert "TestTable" in graphql_type.__name__ or "test_table" in graphql_type.__name__.lower()
|
||||
class TestRowsGraphQLQueryLogic:
|
||||
"""Test business logic for unified table query implementation"""
|
||||
|
||||
def test_sanitize_name_cassandra_compatibility(self):
|
||||
"""Test name sanitization for Cassandra field names"""
|
||||
processor = MagicMock()
|
||||
processor.sanitize_name = Processor.sanitize_name.__get__(processor, Processor)
|
||||
|
||||
# Test field name sanitization (matches storage processor)
|
||||
|
||||
# Test field name sanitization (uses r_ prefix like storage processor)
|
||||
assert processor.sanitize_name("simple_field") == "simple_field"
|
||||
assert processor.sanitize_name("Field-With-Dashes") == "field_with_dashes"
|
||||
assert processor.sanitize_name("field.with.dots") == "field_with_dots"
|
||||
assert processor.sanitize_name("123_field") == "o_123_field"
|
||||
assert processor.sanitize_name("123_field") == "r_123_field"
|
||||
assert processor.sanitize_name("field with spaces") == "field_with_spaces"
|
||||
assert processor.sanitize_name("special!@#chars") == "special___chars"
|
||||
assert processor.sanitize_name("UPPERCASE") == "uppercase"
|
||||
assert processor.sanitize_name("CamelCase") == "camelcase"
|
||||
|
||||
def test_sanitize_table_name(self):
|
||||
"""Test table name sanitization (always gets o_ prefix)"""
|
||||
def test_get_index_names(self):
|
||||
"""Test extraction of index names from schema"""
|
||||
processor = MagicMock()
|
||||
processor.sanitize_table = Processor.sanitize_table.__get__(processor, Processor)
|
||||
|
||||
# Table names always get o_ prefix
|
||||
assert processor.sanitize_table("simple_table") == "o_simple_table"
|
||||
assert processor.sanitize_table("Table-Name") == "o_table_name"
|
||||
assert processor.sanitize_table("123table") == "o_123table"
|
||||
assert processor.sanitize_table("") == "o_"
|
||||
processor.get_index_names = Processor.get_index_names.__get__(processor, Processor)
|
||||
|
||||
schema = RowSchema(
|
||||
name="test_schema",
|
||||
fields=[
|
||||
Field(name="id", type="string", primary=True),
|
||||
Field(name="category", type="string", indexed=True),
|
||||
Field(name="name", type="string"), # Not indexed
|
||||
Field(name="status", type="string", indexed=True)
|
||||
]
|
||||
)
|
||||
|
||||
index_names = processor.get_index_names(schema)
|
||||
|
||||
assert "id" in index_names
|
||||
assert "category" in index_names
|
||||
assert "status" in index_names
|
||||
assert "name" not in index_names
|
||||
assert len(index_names) == 3
|
||||
|
||||
def test_find_matching_index_exact_match(self):
|
||||
"""Test finding matching index for exact match query"""
|
||||
processor = MagicMock()
|
||||
processor.get_index_names = Processor.get_index_names.__get__(processor, Processor)
|
||||
processor.find_matching_index = Processor.find_matching_index.__get__(processor, Processor)
|
||||
|
||||
schema = RowSchema(
|
||||
name="test_schema",
|
||||
fields=[
|
||||
Field(name="id", type="string", primary=True),
|
||||
Field(name="category", type="string", indexed=True),
|
||||
Field(name="name", type="string") # Not indexed
|
||||
]
|
||||
)
|
||||
|
||||
# Filter on indexed field should return match
|
||||
filters = {"category": "electronics"}
|
||||
result = processor.find_matching_index(schema, filters)
|
||||
assert result is not None
|
||||
assert result[0] == "category"
|
||||
assert result[1] == ["electronics"]
|
||||
|
||||
# Filter on non-indexed field should return None
|
||||
filters = {"name": "test"}
|
||||
result = processor.find_matching_index(schema, filters)
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schema_config_parsing(self):
|
||||
"""Test parsing of schema configuration"""
|
||||
processor = MagicMock()
|
||||
processor.schemas = {}
|
||||
processor.graphql_types = {}
|
||||
processor.graphql_schema = None
|
||||
processor.config_key = "schema" # Set the config key
|
||||
processor.generate_graphql_schema = AsyncMock()
|
||||
processor.config_key = "schema"
|
||||
processor.schema_builder = MagicMock()
|
||||
processor.schema_builder.clear = MagicMock()
|
||||
processor.schema_builder.add_schema = MagicMock()
|
||||
processor.schema_builder.build = MagicMock(return_value=MagicMock())
|
||||
processor.on_schema_config = Processor.on_schema_config.__get__(processor, Processor)
|
||||
|
||||
|
||||
# Create test config
|
||||
schema_config = {
|
||||
"schema": {
|
||||
|
|
@ -154,96 +127,29 @@ class TestObjectsGraphQLQueryLogic:
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Process config
|
||||
await processor.on_schema_config(schema_config, version=1)
|
||||
|
||||
|
||||
# Verify schema was loaded
|
||||
assert "customer" in processor.schemas
|
||||
schema = processor.schemas["customer"]
|
||||
assert schema.name == "customer"
|
||||
assert len(schema.fields) == 3
|
||||
|
||||
|
||||
# Verify fields
|
||||
id_field = next(f for f in schema.fields if f.name == "id")
|
||||
assert id_field.primary is True
|
||||
# The field should have been created correctly from JSON
|
||||
# Let's test what we can verify - that the field has the right attributes
|
||||
assert hasattr(id_field, 'required') # Has the required attribute
|
||||
assert hasattr(id_field, 'primary') # Has the primary attribute
|
||||
|
||||
|
||||
email_field = next(f for f in schema.fields if f.name == "email")
|
||||
assert email_field.indexed is True
|
||||
|
||||
|
||||
status_field = next(f for f in schema.fields if f.name == "status")
|
||||
assert status_field.enum_values == ["active", "inactive"]
|
||||
|
||||
# Verify GraphQL schema regeneration was called
|
||||
processor.generate_graphql_schema.assert_called_once()
|
||||
|
||||
def test_cql_query_building_basic(self):
|
||||
"""Test basic CQL query construction"""
|
||||
processor = MagicMock()
|
||||
processor.session = MagicMock()
|
||||
processor.connect_cassandra = MagicMock()
|
||||
processor.sanitize_name = Processor.sanitize_name.__get__(processor, Processor)
|
||||
processor.sanitize_table = Processor.sanitize_table.__get__(processor, Processor)
|
||||
processor.parse_filter_key = Processor.parse_filter_key.__get__(processor, Processor)
|
||||
processor.query_cassandra = Processor.query_cassandra.__get__(processor, Processor)
|
||||
|
||||
# Mock session execute to capture the query
|
||||
mock_result = []
|
||||
processor.session.execute.return_value = mock_result
|
||||
|
||||
# Create test schema
|
||||
schema = RowSchema(
|
||||
name="test_table",
|
||||
fields=[
|
||||
Field(name="id", type="string", primary=True),
|
||||
Field(name="name", type="string", indexed=True),
|
||||
Field(name="status", type="string")
|
||||
]
|
||||
)
|
||||
|
||||
# Test query building
|
||||
asyncio = pytest.importorskip("asyncio")
|
||||
|
||||
async def run_test():
|
||||
await processor.query_cassandra(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
schema_name="test_table",
|
||||
row_schema=schema,
|
||||
filters={"name": "John", "invalid_filter": "ignored"},
|
||||
limit=10
|
||||
)
|
||||
|
||||
# Run the async test
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_until_complete(run_test())
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
# Verify Cassandra connection and query execution
|
||||
processor.connect_cassandra.assert_called_once()
|
||||
processor.session.execute.assert_called_once()
|
||||
|
||||
# Verify the query structure (can't easily test exact query without complex mocking)
|
||||
call_args = processor.session.execute.call_args
|
||||
query = call_args[0][0] # First positional argument is the query
|
||||
params = call_args[0][1] # Second positional argument is parameters
|
||||
|
||||
# Basic query structure checks
|
||||
assert "SELECT * FROM test_user.o_test_table" in query
|
||||
assert "WHERE" in query
|
||||
assert "collection = %s" in query
|
||||
assert "LIMIT 10" in query
|
||||
|
||||
# Parameters should include collection and name filter
|
||||
assert "test_collection" in params
|
||||
assert "John" in params
|
||||
# Verify schema builder was called
|
||||
processor.schema_builder.add_schema.assert_called_once()
|
||||
processor.schema_builder.build.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_graphql_context_handling(self):
|
||||
|
|
@ -251,13 +157,13 @@ class TestObjectsGraphQLQueryLogic:
|
|||
processor = MagicMock()
|
||||
processor.graphql_schema = AsyncMock()
|
||||
processor.execute_graphql_query = Processor.execute_graphql_query.__get__(processor, Processor)
|
||||
|
||||
|
||||
# Mock schema execution
|
||||
mock_result = MagicMock()
|
||||
mock_result.data = {"customers": [{"id": "1", "name": "Test"}]}
|
||||
mock_result.errors = None
|
||||
processor.graphql_schema.execute.return_value = mock_result
|
||||
|
||||
|
||||
result = await processor.execute_graphql_query(
|
||||
query='{ customers { id name } }',
|
||||
variables={},
|
||||
|
|
@ -265,17 +171,17 @@ class TestObjectsGraphQLQueryLogic:
|
|||
user="test_user",
|
||||
collection="test_collection"
|
||||
)
|
||||
|
||||
|
||||
# Verify schema.execute was called with correct context
|
||||
processor.graphql_schema.execute.assert_called_once()
|
||||
call_args = processor.graphql_schema.execute.call_args
|
||||
|
||||
|
||||
# Verify context was passed
|
||||
context = call_args[1]['context_value'] # keyword argument
|
||||
context = call_args[1]['context_value']
|
||||
assert context["processor"] == processor
|
||||
assert context["user"] == "test_user"
|
||||
assert context["collection"] == "test_collection"
|
||||
|
||||
|
||||
# Verify result structure
|
||||
assert "data" in result
|
||||
assert result["data"] == {"customers": [{"id": "1", "name": "Test"}]}
|
||||
|
|
@ -286,104 +192,79 @@ class TestObjectsGraphQLQueryLogic:
|
|||
processor = MagicMock()
|
||||
processor.graphql_schema = AsyncMock()
|
||||
processor.execute_graphql_query = Processor.execute_graphql_query.__get__(processor, Processor)
|
||||
|
||||
# Create a simple object to simulate GraphQL error instead of MagicMock
|
||||
|
||||
# Create a simple object to simulate GraphQL error
|
||||
class MockError:
|
||||
def __init__(self, message, path, extensions):
|
||||
self.message = message
|
||||
self.path = path
|
||||
self.extensions = extensions
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
|
||||
mock_error = MockError(
|
||||
message="Field 'invalid_field' doesn't exist",
|
||||
path=["customers", "0", "invalid_field"],
|
||||
extensions={"code": "FIELD_NOT_FOUND"}
|
||||
)
|
||||
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.data = None
|
||||
mock_result.errors = [mock_error]
|
||||
processor.graphql_schema.execute.return_value = mock_result
|
||||
|
||||
|
||||
result = await processor.execute_graphql_query(
|
||||
query='{ customers { invalid_field } }',
|
||||
variables={},
|
||||
operation_name=None,
|
||||
user="test_user",
|
||||
user="test_user",
|
||||
collection="test_collection"
|
||||
)
|
||||
|
||||
|
||||
# Verify error handling
|
||||
assert "errors" in result
|
||||
assert len(result["errors"]) == 1
|
||||
|
||||
|
||||
error = result["errors"][0]
|
||||
assert error["message"] == "Field 'invalid_field' doesn't exist"
|
||||
assert error["path"] == ["customers", "0", "invalid_field"] # Fixed to match string path
|
||||
assert error["path"] == ["customers", "0", "invalid_field"]
|
||||
assert error["extensions"] == {"code": "FIELD_NOT_FOUND"}
|
||||
|
||||
def test_schema_generation_basic_structure(self):
|
||||
"""Test basic GraphQL schema generation structure"""
|
||||
processor = MagicMock()
|
||||
processor.schemas = {
|
||||
"customer": RowSchema(
|
||||
name="customer",
|
||||
fields=[
|
||||
Field(name="id", type="string", primary=True),
|
||||
Field(name="name", type="string")
|
||||
]
|
||||
)
|
||||
}
|
||||
processor.graphql_types = {}
|
||||
processor.get_python_type = Processor.get_python_type.__get__(processor, Processor)
|
||||
processor.create_graphql_type = Processor.create_graphql_type.__get__(processor, Processor)
|
||||
|
||||
# Test individual type creation (avoiding the full schema generation which has annotation issues)
|
||||
graphql_type = processor.create_graphql_type("customer", processor.schemas["customer"])
|
||||
processor.graphql_types["customer"] = graphql_type
|
||||
|
||||
# Verify type was created
|
||||
assert len(processor.graphql_types) == 1
|
||||
assert "customer" in processor.graphql_types
|
||||
assert processor.graphql_types["customer"] is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_message_processing_success(self):
|
||||
"""Test successful message processing flow"""
|
||||
processor = MagicMock()
|
||||
processor.execute_graphql_query = AsyncMock()
|
||||
processor.on_message = Processor.on_message.__get__(processor, Processor)
|
||||
|
||||
|
||||
# Mock successful query result
|
||||
processor.execute_graphql_query.return_value = {
|
||||
"data": {"customers": [{"id": "1", "name": "John"}]},
|
||||
"errors": [],
|
||||
"extensions": {"execution_time": "0.1"} # Extensions must be strings for Map(String())
|
||||
"extensions": {}
|
||||
}
|
||||
|
||||
|
||||
# Create mock message
|
||||
mock_msg = MagicMock()
|
||||
mock_request = ObjectsQueryRequest(
|
||||
mock_request = RowsQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
collection="test_collection",
|
||||
query='{ customers { id name } }',
|
||||
variables={},
|
||||
operation_name=None
|
||||
)
|
||||
mock_msg.value.return_value = mock_request
|
||||
mock_msg.properties.return_value = {"id": "test-123"}
|
||||
|
||||
|
||||
# Mock flow
|
||||
mock_flow = MagicMock()
|
||||
mock_response_flow = AsyncMock()
|
||||
mock_flow.return_value = mock_response_flow
|
||||
|
||||
|
||||
# Process message
|
||||
await processor.on_message(mock_msg, None, mock_flow)
|
||||
|
||||
|
||||
# Verify query was executed
|
||||
processor.execute_graphql_query.assert_called_once_with(
|
||||
query='{ customers { id name } }',
|
||||
|
|
@ -392,13 +273,13 @@ class TestObjectsGraphQLQueryLogic:
|
|||
user="test_user",
|
||||
collection="test_collection"
|
||||
)
|
||||
|
||||
|
||||
# Verify response was sent
|
||||
mock_response_flow.send.assert_called_once()
|
||||
response_call = mock_response_flow.send.call_args[0][0]
|
||||
|
||||
|
||||
# Verify response structure
|
||||
assert isinstance(response_call, ObjectsQueryResponse)
|
||||
assert isinstance(response_call, RowsQueryResponse)
|
||||
assert response_call.error is None
|
||||
assert '"customers"' in response_call.data # JSON encoded
|
||||
assert len(response_call.errors) == 0
|
||||
|
|
@ -409,13 +290,13 @@ class TestObjectsGraphQLQueryLogic:
|
|||
processor = MagicMock()
|
||||
processor.execute_graphql_query = AsyncMock()
|
||||
processor.on_message = Processor.on_message.__get__(processor, Processor)
|
||||
|
||||
|
||||
# Mock query execution error
|
||||
processor.execute_graphql_query.side_effect = RuntimeError("No schema available")
|
||||
|
||||
|
||||
# Create mock message
|
||||
mock_msg = MagicMock()
|
||||
mock_request = ObjectsQueryRequest(
|
||||
mock_request = RowsQueryRequest(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
query='{ invalid_query }',
|
||||
|
|
@ -424,67 +305,225 @@ class TestObjectsGraphQLQueryLogic:
|
|||
)
|
||||
mock_msg.value.return_value = mock_request
|
||||
mock_msg.properties.return_value = {"id": "test-456"}
|
||||
|
||||
|
||||
# Mock flow
|
||||
mock_flow = MagicMock()
|
||||
mock_response_flow = AsyncMock()
|
||||
mock_flow.return_value = mock_response_flow
|
||||
|
||||
|
||||
# Process message
|
||||
await processor.on_message(mock_msg, None, mock_flow)
|
||||
|
||||
|
||||
# Verify error response was sent
|
||||
mock_response_flow.send.assert_called_once()
|
||||
response_call = mock_response_flow.send.call_args[0][0]
|
||||
|
||||
|
||||
# Verify error response structure
|
||||
assert isinstance(response_call, ObjectsQueryResponse)
|
||||
assert isinstance(response_call, RowsQueryResponse)
|
||||
assert response_call.error is not None
|
||||
assert response_call.error.type == "objects-query-error"
|
||||
assert response_call.error.type == "rows-query-error"
|
||||
assert "No schema available" in response_call.error.message
|
||||
assert response_call.data is None
|
||||
|
||||
|
||||
class TestCQLQueryGeneration:
|
||||
"""Test CQL query generation logic in isolation"""
|
||||
|
||||
def test_partition_key_inclusion(self):
|
||||
"""Test that collection is always included in queries"""
|
||||
class TestUnifiedTableQueries:
|
||||
"""Test queries against the unified rows table"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_with_index_match(self):
|
||||
"""Test query execution with matching index"""
|
||||
processor = MagicMock()
|
||||
processor.session = MagicMock()
|
||||
processor.connect_cassandra = MagicMock()
|
||||
processor.sanitize_name = Processor.sanitize_name.__get__(processor, Processor)
|
||||
processor.sanitize_table = Processor.sanitize_table.__get__(processor, Processor)
|
||||
|
||||
# Mock the query building (simplified version)
|
||||
keyspace = processor.sanitize_name("test_user")
|
||||
table = processor.sanitize_table("test_table")
|
||||
|
||||
query = f"SELECT * FROM {keyspace}.{table}"
|
||||
where_clauses = ["collection = %s"]
|
||||
|
||||
assert "collection = %s" in where_clauses
|
||||
assert keyspace == "test_user"
|
||||
assert table == "o_test_table"
|
||||
|
||||
processor.get_index_names = Processor.get_index_names.__get__(processor, Processor)
|
||||
processor.find_matching_index = Processor.find_matching_index.__get__(processor, Processor)
|
||||
processor.query_cassandra = Processor.query_cassandra.__get__(processor, Processor)
|
||||
|
||||
# Mock session execute to return test data
|
||||
mock_row = MagicMock()
|
||||
mock_row.data = {"id": "123", "name": "Test Product", "category": "electronics"}
|
||||
processor.session.execute.return_value = [mock_row]
|
||||
|
||||
schema = RowSchema(
|
||||
name="products",
|
||||
fields=[
|
||||
Field(name="id", type="string", primary=True),
|
||||
Field(name="category", type="string", indexed=True),
|
||||
Field(name="name", type="string")
|
||||
]
|
||||
)
|
||||
|
||||
# Query with filter on indexed field
|
||||
results = await processor.query_cassandra(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
schema_name="products",
|
||||
row_schema=schema,
|
||||
filters={"category": "electronics"},
|
||||
limit=10
|
||||
)
|
||||
|
||||
# Verify Cassandra was connected and queried
|
||||
processor.connect_cassandra.assert_called_once()
|
||||
processor.session.execute.assert_called_once()
|
||||
|
||||
# Verify query structure - should query unified rows table
|
||||
call_args = processor.session.execute.call_args
|
||||
query = call_args[0][0]
|
||||
params = call_args[0][1]
|
||||
|
||||
assert "SELECT data, source FROM test_user.rows" in query
|
||||
assert "collection = %s" in query
|
||||
assert "schema_name = %s" in query
|
||||
assert "index_name = %s" in query
|
||||
assert "index_value = %s" in query
|
||||
|
||||
assert params[0] == "test_collection"
|
||||
assert params[1] == "products"
|
||||
assert params[2] == "category"
|
||||
assert params[3] == ["electronics"]
|
||||
|
||||
# Verify results
|
||||
assert len(results) == 1
|
||||
assert results[0]["id"] == "123"
|
||||
assert results[0]["category"] == "electronics"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_without_index_match(self):
|
||||
"""Test query execution without matching index (scan mode)"""
|
||||
processor = MagicMock()
|
||||
processor.session = MagicMock()
|
||||
processor.connect_cassandra = MagicMock()
|
||||
processor.sanitize_name = Processor.sanitize_name.__get__(processor, Processor)
|
||||
processor.get_index_names = Processor.get_index_names.__get__(processor, Processor)
|
||||
processor.find_matching_index = Processor.find_matching_index.__get__(processor, Processor)
|
||||
processor._matches_filters = Processor._matches_filters.__get__(processor, Processor)
|
||||
processor.query_cassandra = Processor.query_cassandra.__get__(processor, Processor)
|
||||
|
||||
# Mock session execute to return test data
|
||||
mock_row1 = MagicMock()
|
||||
mock_row1.data = {"id": "1", "name": "Product A", "price": "100"}
|
||||
mock_row2 = MagicMock()
|
||||
mock_row2.data = {"id": "2", "name": "Product B", "price": "200"}
|
||||
processor.session.execute.return_value = [mock_row1, mock_row2]
|
||||
|
||||
schema = RowSchema(
|
||||
name="products",
|
||||
fields=[
|
||||
Field(name="id", type="string", primary=True),
|
||||
Field(name="name", type="string"), # Not indexed
|
||||
Field(name="price", type="string") # Not indexed
|
||||
]
|
||||
)
|
||||
|
||||
# Query with filter on non-indexed field
|
||||
results = await processor.query_cassandra(
|
||||
user="test_user",
|
||||
collection="test_collection",
|
||||
schema_name="products",
|
||||
row_schema=schema,
|
||||
filters={"name": "Product A"},
|
||||
limit=10
|
||||
)
|
||||
|
||||
# Query should use ALLOW FILTERING for scan
|
||||
call_args = processor.session.execute.call_args
|
||||
query = call_args[0][0]
|
||||
|
||||
assert "ALLOW FILTERING" in query
|
||||
|
||||
# Should post-filter results
|
||||
assert len(results) == 1
|
||||
assert results[0]["name"] == "Product A"
|
||||
|
||||
|
||||
class TestFilterMatching:
|
||||
"""Test filter matching logic"""
|
||||
|
||||
def test_matches_filters_exact_match(self):
|
||||
"""Test exact match filter"""
|
||||
processor = MagicMock()
|
||||
processor._matches_filters = Processor._matches_filters.__get__(processor, Processor)
|
||||
|
||||
schema = RowSchema(name="test", fields=[Field(name="status", type="string")])
|
||||
|
||||
row = {"status": "active", "name": "test"}
|
||||
assert processor._matches_filters(row, {"status": "active"}, schema) is True
|
||||
assert processor._matches_filters(row, {"status": "inactive"}, schema) is False
|
||||
|
||||
def test_matches_filters_comparison_operators(self):
|
||||
"""Test comparison operators in filters"""
|
||||
processor = MagicMock()
|
||||
processor._matches_filters = Processor._matches_filters.__get__(processor, Processor)
|
||||
|
||||
schema = RowSchema(name="test", fields=[Field(name="price", type="float")])
|
||||
|
||||
row = {"price": "100.0"}
|
||||
|
||||
# Greater than
|
||||
assert processor._matches_filters(row, {"price_gt": 50}, schema) is True
|
||||
assert processor._matches_filters(row, {"price_gt": 150}, schema) is False
|
||||
|
||||
# Less than
|
||||
assert processor._matches_filters(row, {"price_lt": 150}, schema) is True
|
||||
assert processor._matches_filters(row, {"price_lt": 50}, schema) is False
|
||||
|
||||
# Greater than or equal
|
||||
assert processor._matches_filters(row, {"price_gte": 100}, schema) is True
|
||||
assert processor._matches_filters(row, {"price_gte": 101}, schema) is False
|
||||
|
||||
# Less than or equal
|
||||
assert processor._matches_filters(row, {"price_lte": 100}, schema) is True
|
||||
assert processor._matches_filters(row, {"price_lte": 99}, schema) is False
|
||||
|
||||
def test_matches_filters_contains(self):
|
||||
"""Test contains filter"""
|
||||
processor = MagicMock()
|
||||
processor._matches_filters = Processor._matches_filters.__get__(processor, Processor)
|
||||
|
||||
schema = RowSchema(name="test", fields=[Field(name="description", type="string")])
|
||||
|
||||
row = {"description": "A great product for everyone"}
|
||||
|
||||
assert processor._matches_filters(row, {"description_contains": "great"}, schema) is True
|
||||
assert processor._matches_filters(row, {"description_contains": "terrible"}, schema) is False
|
||||
|
||||
def test_matches_filters_in_list(self):
|
||||
"""Test in-list filter"""
|
||||
processor = MagicMock()
|
||||
processor._matches_filters = Processor._matches_filters.__get__(processor, Processor)
|
||||
|
||||
schema = RowSchema(name="test", fields=[Field(name="status", type="string")])
|
||||
|
||||
row = {"status": "active"}
|
||||
|
||||
assert processor._matches_filters(row, {"status_in": ["active", "pending"]}, schema) is True
|
||||
assert processor._matches_filters(row, {"status_in": ["inactive", "deleted"]}, schema) is False
|
||||
|
||||
|
||||
class TestIndexedFieldFiltering:
|
||||
"""Test that only indexed or primary key fields can be directly filtered"""
|
||||
|
||||
def test_indexed_field_filtering(self):
|
||||
"""Test that only indexed or primary key fields can be filtered"""
|
||||
# Create schema with mixed field types
|
||||
schema = RowSchema(
|
||||
name="test",
|
||||
fields=[
|
||||
Field(name="id", type="string", primary=True),
|
||||
Field(name="indexed_field", type="string", indexed=True),
|
||||
Field(name="indexed_field", type="string", indexed=True),
|
||||
Field(name="normal_field", type="string", indexed=False),
|
||||
Field(name="another_field", type="string")
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
filters = {
|
||||
"id": "test123", # Primary key - should be included
|
||||
"indexed_field": "value", # Indexed - should be included
|
||||
"normal_field": "ignored", # Not indexed - should be ignored
|
||||
"another_field": "also_ignored" # Not indexed - should be ignored
|
||||
}
|
||||
|
||||
|
||||
# Simulate the filtering logic from the processor
|
||||
valid_filters = []
|
||||
for field_name, value in filters.items():
|
||||
|
|
@ -492,7 +531,7 @@ class TestCQLQueryGeneration:
|
|||
schema_field = next((f for f in schema.fields if f.name == field_name), None)
|
||||
if schema_field and (schema_field.indexed or schema_field.primary):
|
||||
valid_filters.append((field_name, value))
|
||||
|
||||
|
||||
# Only id and indexed_field should be included
|
||||
assert len(valid_filters) == 2
|
||||
field_names = [f[0] for f in valid_filters]
|
||||
|
|
@ -500,52 +539,3 @@ class TestCQLQueryGeneration:
|
|||
assert "indexed_field" in field_names
|
||||
assert "normal_field" not in field_names
|
||||
assert "another_field" not in field_names
|
||||
|
||||
|
||||
class TestGraphQLSchemaGeneration:
|
||||
"""Test GraphQL schema generation in detail"""
|
||||
|
||||
def test_field_type_annotations(self):
|
||||
"""Test that GraphQL types have correct field annotations"""
|
||||
processor = MagicMock()
|
||||
processor.get_python_type = Processor.get_python_type.__get__(processor, Processor)
|
||||
processor.create_graphql_type = Processor.create_graphql_type.__get__(processor, Processor)
|
||||
|
||||
# Create schema with various field types
|
||||
schema = RowSchema(
|
||||
name="test",
|
||||
fields=[
|
||||
Field(name="id", type="string", required=True, primary=True),
|
||||
Field(name="count", type="integer", required=True),
|
||||
Field(name="price", type="float", required=False),
|
||||
Field(name="active", type="boolean", required=False),
|
||||
Field(name="optional_text", type="string", required=False)
|
||||
]
|
||||
)
|
||||
|
||||
# Create GraphQL type
|
||||
graphql_type = processor.create_graphql_type("test", schema)
|
||||
|
||||
# Verify type was created successfully
|
||||
assert graphql_type is not None
|
||||
|
||||
def test_basic_type_creation(self):
|
||||
"""Test that GraphQL types are created correctly"""
|
||||
processor = MagicMock()
|
||||
processor.schemas = {
|
||||
"customer": RowSchema(
|
||||
name="customer",
|
||||
fields=[Field(name="id", type="string", primary=True)]
|
||||
)
|
||||
}
|
||||
processor.graphql_types = {}
|
||||
processor.get_python_type = Processor.get_python_type.__get__(processor, Processor)
|
||||
processor.create_graphql_type = Processor.create_graphql_type.__get__(processor, Processor)
|
||||
|
||||
# Create GraphQL type directly
|
||||
graphql_type = processor.create_graphql_type("customer", processor.schemas["customer"])
|
||||
processor.graphql_types["customer"] = graphql_type
|
||||
|
||||
# Verify customer type was created
|
||||
assert "customer" in processor.graphql_types
|
||||
assert processor.graphql_types["customer"] is not None
|
||||
|
|
@ -5,8 +5,8 @@ Tests for Cassandra triples query service
|
|||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from trustgraph.query.triples.cassandra.service import Processor
|
||||
from trustgraph.schema import Value
|
||||
from trustgraph.query.triples.cassandra.service import Processor, create_term
|
||||
from trustgraph.schema import Term, IRI, LITERAL
|
||||
|
||||
|
||||
class TestCassandraQueryProcessor:
|
||||
|
|
@ -21,94 +21,101 @@ class TestCassandraQueryProcessor:
|
|||
graph_host='localhost'
|
||||
)
|
||||
|
||||
def test_create_value_with_http_uri(self, processor):
|
||||
"""Test create_value with HTTP URI"""
|
||||
result = processor.create_value("http://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "http://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
def test_create_term_with_http_uri(self, processor):
|
||||
"""Test create_term with HTTP URI"""
|
||||
result = create_term("http://example.com/resource")
|
||||
|
||||
def test_create_value_with_https_uri(self, processor):
|
||||
"""Test create_value with HTTPS URI"""
|
||||
result = processor.create_value("https://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "https://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "http://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_value_with_literal(self, processor):
|
||||
"""Test create_value with literal value"""
|
||||
result = processor.create_value("just a literal string")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
def test_create_term_with_https_uri(self, processor):
|
||||
"""Test create_term with HTTPS URI"""
|
||||
result = create_term("https://example.com/resource")
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "https://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_term_with_literal(self, processor):
|
||||
"""Test create_term with literal value"""
|
||||
result = create_term("just a literal string")
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "just a literal string"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_empty_string(self, processor):
|
||||
"""Test create_value with empty string"""
|
||||
result = processor.create_value("")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
def test_create_term_with_empty_string(self, processor):
|
||||
"""Test create_term with empty string"""
|
||||
result = create_term("")
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == ""
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_partial_uri(self, processor):
|
||||
"""Test create_value with string that looks like URI but isn't complete"""
|
||||
result = processor.create_value("http")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
def test_create_term_with_partial_uri(self, processor):
|
||||
"""Test create_term with string that looks like URI but isn't complete"""
|
||||
result = create_term("http")
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "http"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_ftp_uri(self, processor):
|
||||
"""Test create_value with FTP URI (should not be detected as URI)"""
|
||||
result = processor.create_value("ftp://example.com/file")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
def test_create_term_with_ftp_uri(self, processor):
|
||||
"""Test create_term with FTP URI (should not be detected as URI)"""
|
||||
result = create_term("ftp://example.com/file")
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "ftp://example.com/file"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_spo_query(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_spo_query(self, mock_kg_class):
|
||||
"""Test querying triples with subject, predicate, and object specified"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
# Setup mock TrustGraph
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
# Setup mock TrustGraph via factory function
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
mock_tg_instance.get_spo.return_value = None # SPO query returns None if found
|
||||
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
# SPO query returns a list of results (with mock graph attribute)
|
||||
mock_result = MagicMock()
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_result.o = 'test_object'
|
||||
mock_tg_instance.get_spo.return_value = [mock_result]
|
||||
|
||||
processor = Processor(
|
||||
taskgroup=MagicMock(),
|
||||
id='test-cassandra-query',
|
||||
cassandra_host='localhost'
|
||||
)
|
||||
|
||||
|
||||
# Create query request with all SPO values
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
p=Value(value='test_predicate', is_uri=False),
|
||||
o=Value(value='test_object', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=Term(type=LITERAL, value='test_predicate'),
|
||||
o=Term(type=LITERAL, value='test_object'),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
||||
result = await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify KnowledgeGraph was created with correct parameters
|
||||
mock_trustgraph.assert_called_once_with(
|
||||
mock_kg_class.assert_called_once_with(
|
||||
hosts=['localhost'],
|
||||
keyspace='test_user'
|
||||
)
|
||||
|
||||
|
||||
# Verify get_spo was called with correct parameters
|
||||
mock_tg_instance.get_spo.assert_called_once_with(
|
||||
'test_collection', 'test_subject', 'test_predicate', 'test_object', limit=100
|
||||
'test_collection', 'test_subject', 'test_predicate', 'test_object', g=None, limit=100
|
||||
)
|
||||
|
||||
|
||||
# Verify result contains the queried triple
|
||||
assert len(result) == 1
|
||||
assert result[0].s.value == 'test_subject'
|
||||
|
|
@ -143,154 +150,174 @@ class TestCassandraQueryProcessor:
|
|||
assert processor.table is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_sp_pattern(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_sp_pattern(self, mock_kg_class):
|
||||
"""Test SP query pattern (subject and predicate, no object)"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
# Setup mock TrustGraph and response
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
# Setup mock TrustGraph via factory function
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.o = 'result_object'
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_tg_instance.get_sp.return_value = [mock_result]
|
||||
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
||||
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
p=Value(value='test_predicate', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=Term(type=LITERAL, value='test_predicate'),
|
||||
o=None,
|
||||
limit=50
|
||||
)
|
||||
|
||||
|
||||
result = await processor.query_triples(query)
|
||||
|
||||
mock_tg_instance.get_sp.assert_called_once_with('test_collection', 'test_subject', 'test_predicate', limit=50)
|
||||
|
||||
mock_tg_instance.get_sp.assert_called_once_with('test_collection', 'test_subject', 'test_predicate', g=None, limit=50)
|
||||
assert len(result) == 1
|
||||
assert result[0].s.value == 'test_subject'
|
||||
assert result[0].p.value == 'test_predicate'
|
||||
assert result[0].o.value == 'result_object'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_s_pattern(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_s_pattern(self, mock_kg_class):
|
||||
"""Test S query pattern (subject only)"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.p = 'result_predicate'
|
||||
mock_result.o = 'result_object'
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_tg_instance.get_s.return_value = [mock_result]
|
||||
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
||||
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=25
|
||||
)
|
||||
|
||||
|
||||
result = await processor.query_triples(query)
|
||||
|
||||
mock_tg_instance.get_s.assert_called_once_with('test_collection', 'test_subject', limit=25)
|
||||
|
||||
mock_tg_instance.get_s.assert_called_once_with('test_collection', 'test_subject', g=None, limit=25)
|
||||
assert len(result) == 1
|
||||
assert result[0].s.value == 'test_subject'
|
||||
assert result[0].p.value == 'result_predicate'
|
||||
assert result[0].o.value == 'result_object'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_p_pattern(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_p_pattern(self, mock_kg_class):
|
||||
"""Test P query pattern (predicate only)"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.s = 'result_subject'
|
||||
mock_result.o = 'result_object'
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_tg_instance.get_p.return_value = [mock_result]
|
||||
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
||||
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=None,
|
||||
p=Value(value='test_predicate', is_uri=False),
|
||||
p=Term(type=LITERAL, value='test_predicate'),
|
||||
o=None,
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
||||
result = await processor.query_triples(query)
|
||||
|
||||
mock_tg_instance.get_p.assert_called_once_with('test_collection', 'test_predicate', limit=10)
|
||||
|
||||
mock_tg_instance.get_p.assert_called_once_with('test_collection', 'test_predicate', g=None, limit=10)
|
||||
assert len(result) == 1
|
||||
assert result[0].s.value == 'result_subject'
|
||||
assert result[0].p.value == 'test_predicate'
|
||||
assert result[0].o.value == 'result_object'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_o_pattern(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_o_pattern(self, mock_kg_class):
|
||||
"""Test O query pattern (object only)"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.s = 'result_subject'
|
||||
mock_result.p = 'result_predicate'
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_tg_instance.get_o.return_value = [mock_result]
|
||||
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
||||
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=None,
|
||||
p=None,
|
||||
o=Value(value='test_object', is_uri=False),
|
||||
o=Term(type=LITERAL, value='test_object'),
|
||||
limit=75
|
||||
)
|
||||
|
||||
|
||||
result = await processor.query_triples(query)
|
||||
|
||||
mock_tg_instance.get_o.assert_called_once_with('test_collection', 'test_object', limit=75)
|
||||
|
||||
mock_tg_instance.get_o.assert_called_once_with('test_collection', 'test_object', g=None, limit=75)
|
||||
assert len(result) == 1
|
||||
assert result[0].s.value == 'result_subject'
|
||||
assert result[0].p.value == 'result_predicate'
|
||||
assert result[0].o.value == 'test_object'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_get_all_pattern(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_get_all_pattern(self, mock_kg_class):
|
||||
"""Test query pattern with no constraints (get all)"""
|
||||
from trustgraph.schema import TriplesQueryRequest
|
||||
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.s = 'all_subject'
|
||||
mock_result.p = 'all_predicate'
|
||||
mock_result.o = 'all_object'
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_tg_instance.get_all.return_value = [mock_result]
|
||||
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
||||
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
|
|
@ -299,9 +326,9 @@ class TestCassandraQueryProcessor:
|
|||
o=None,
|
||||
limit=1000
|
||||
)
|
||||
|
||||
|
||||
result = await processor.query_triples(query)
|
||||
|
||||
|
||||
mock_tg_instance.get_all.assert_called_once_with('test_collection', limit=1000)
|
||||
assert len(result) == 1
|
||||
assert result[0].s.value == 'all_subject'
|
||||
|
|
@ -372,37 +399,44 @@ class TestCassandraQueryProcessor:
|
|||
|
||||
run()
|
||||
|
||||
mock_launch.assert_called_once_with(default_ident, '\nTriples query service. Input is a (s, p, o) triple, some values may be\nnull. Output is a list of triples.\n')
|
||||
mock_launch.assert_called_once_with(default_ident, '\nTriples query service. Input is a (s, p, o, g) quad pattern, some values may be\nnull. Output is a list of quads.\n')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_with_authentication(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_with_authentication(self, mock_kg_class):
|
||||
"""Test querying with username and password authentication"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
mock_tg_instance.get_spo.return_value = None
|
||||
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
# SPO query returns a list of results
|
||||
mock_result = MagicMock()
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_result.o = 'test_object'
|
||||
mock_tg_instance.get_spo.return_value = [mock_result]
|
||||
|
||||
processor = Processor(
|
||||
taskgroup=MagicMock(),
|
||||
cassandra_username='authuser',
|
||||
cassandra_password='authpass'
|
||||
)
|
||||
|
||||
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
p=Value(value='test_predicate', is_uri=False),
|
||||
o=Value(value='test_object', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=Term(type=LITERAL, value='test_predicate'),
|
||||
o=Term(type=LITERAL, value='test_object'),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
||||
await processor.query_triples(query)
|
||||
|
||||
|
||||
# Verify KnowledgeGraph was created with authentication
|
||||
mock_trustgraph.assert_called_once_with(
|
||||
mock_kg_class.assert_called_once_with(
|
||||
hosts=['cassandra'], # Updated default
|
||||
keyspace='test_user',
|
||||
username='authuser',
|
||||
|
|
@ -410,128 +444,154 @@ class TestCassandraQueryProcessor:
|
|||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_table_reuse(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_table_reuse(self, mock_kg_class):
|
||||
"""Test that TrustGraph is reused for same table"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
mock_tg_instance.get_spo.return_value = None
|
||||
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
# SPO query returns a list of results
|
||||
mock_result = MagicMock()
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_result.o = 'test_object'
|
||||
mock_tg_instance.get_spo.return_value = [mock_result]
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
||||
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
p=Value(value='test_predicate', is_uri=False),
|
||||
o=Value(value='test_object', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=Term(type=LITERAL, value='test_predicate'),
|
||||
o=Term(type=LITERAL, value='test_object'),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
||||
# First query should create TrustGraph
|
||||
await processor.query_triples(query)
|
||||
assert mock_trustgraph.call_count == 1
|
||||
|
||||
assert mock_kg_class.call_count == 1
|
||||
|
||||
# Second query with same table should reuse TrustGraph
|
||||
await processor.query_triples(query)
|
||||
assert mock_trustgraph.call_count == 1 # Should not increase
|
||||
assert mock_kg_class.call_count == 1 # Should not increase
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_table_switching(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_table_switching(self, mock_kg_class):
|
||||
"""Test table switching creates new TrustGraph"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance1 = MagicMock()
|
||||
mock_tg_instance2 = MagicMock()
|
||||
mock_trustgraph.side_effect = [mock_tg_instance1, mock_tg_instance2]
|
||||
|
||||
mock_kg_class.side_effect = [mock_tg_instance1, mock_tg_instance2]
|
||||
|
||||
# Setup mock results for both instances
|
||||
mock_result = MagicMock()
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_result.p = 'p'
|
||||
mock_result.o = 'o'
|
||||
mock_tg_instance1.get_s.return_value = [mock_result]
|
||||
mock_tg_instance2.get_s.return_value = [mock_result]
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
||||
|
||||
# First query
|
||||
query1 = TriplesQueryRequest(
|
||||
user='user1',
|
||||
collection='collection1',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
||||
await processor.query_triples(query1)
|
||||
assert processor.table == 'user1'
|
||||
|
||||
|
||||
# Second query with different table
|
||||
query2 = TriplesQueryRequest(
|
||||
user='user2',
|
||||
collection='collection2',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
||||
await processor.query_triples(query2)
|
||||
assert processor.table == 'user2'
|
||||
|
||||
|
||||
# Verify TrustGraph was created twice
|
||||
assert mock_trustgraph.call_count == 2
|
||||
assert mock_kg_class.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_exception_handling(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_exception_handling(self, mock_kg_class):
|
||||
"""Test exception handling during query execution"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
mock_tg_instance.get_spo.side_effect = Exception("Query failed")
|
||||
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
||||
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
p=Value(value='test_predicate', is_uri=False),
|
||||
o=Value(value='test_object', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=Term(type=LITERAL, value='test_predicate'),
|
||||
o=Term(type=LITERAL, value='test_object'),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
||||
with pytest.raises(Exception, match="Query failed"):
|
||||
await processor.query_triples(query)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_query_triples_multiple_results(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_query_triples_multiple_results(self, mock_kg_class):
|
||||
"""Test query returning multiple results"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
# Mock multiple results
|
||||
mock_result1 = MagicMock()
|
||||
mock_result1.o = 'object1'
|
||||
mock_result1.g = ''
|
||||
mock_result1.otype = None
|
||||
mock_result1.dtype = None
|
||||
mock_result1.lang = None
|
||||
mock_result2 = MagicMock()
|
||||
mock_result2.o = 'object2'
|
||||
mock_result2.g = ''
|
||||
mock_result2.otype = None
|
||||
mock_result2.dtype = None
|
||||
mock_result2.lang = None
|
||||
mock_tg_instance.get_sp.return_value = [mock_result1, mock_result2]
|
||||
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
||||
|
||||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
p=Value(value='test_predicate', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=Term(type=LITERAL, value='test_predicate'),
|
||||
o=None,
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
||||
result = await processor.query_triples(query)
|
||||
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0].o.value == 'object1'
|
||||
assert result[1].o.value == 'object2'
|
||||
|
|
@ -541,16 +601,20 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
"""Test cases for multi-table performance optimizations in query service"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_get_po_query_optimization(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_get_po_query_optimization(self, mock_kg_class):
|
||||
"""Test that get_po queries use optimized table (no ALLOW FILTERING)"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.s = 'result_subject'
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_tg_instance.get_po.return_value = [mock_result]
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
|
@ -560,8 +624,8 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=None,
|
||||
p=Value(value='test_predicate', is_uri=False),
|
||||
o=Value(value='test_object', is_uri=False),
|
||||
p=Term(type=LITERAL, value='test_predicate'),
|
||||
o=Term(type=LITERAL, value='test_object'),
|
||||
limit=50
|
||||
)
|
||||
|
||||
|
|
@ -569,7 +633,7 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
|
||||
# Verify get_po was called (should use optimized po_table)
|
||||
mock_tg_instance.get_po.assert_called_once_with(
|
||||
'test_collection', 'test_predicate', 'test_object', limit=50
|
||||
'test_collection', 'test_predicate', 'test_object', g=None, limit=50
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
|
|
@ -578,16 +642,20 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
assert result[0].o.value == 'test_object'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_get_os_query_optimization(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_get_os_query_optimization(self, mock_kg_class):
|
||||
"""Test that get_os queries use optimized table (no ALLOW FILTERING)"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.p = 'result_predicate'
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_tg_instance.get_os.return_value = [mock_result]
|
||||
|
||||
processor = Processor(taskgroup=MagicMock())
|
||||
|
|
@ -596,9 +664,9 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value='test_subject', is_uri=False),
|
||||
s=Term(type=LITERAL, value='test_subject'),
|
||||
p=None,
|
||||
o=Value(value='test_object', is_uri=False),
|
||||
o=Term(type=LITERAL, value='test_object'),
|
||||
limit=25
|
||||
)
|
||||
|
||||
|
|
@ -606,7 +674,7 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
|
||||
# Verify get_os was called (should use optimized subject_table with clustering)
|
||||
mock_tg_instance.get_os.assert_called_once_with(
|
||||
'test_collection', 'test_object', 'test_subject', limit=25
|
||||
'test_collection', 'test_object', 'test_subject', g=None, limit=25
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
|
|
@ -615,13 +683,13 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
assert result[0].o.value == 'test_object'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_all_query_patterns_use_correct_tables(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_all_query_patterns_use_correct_tables(self, mock_kg_class):
|
||||
"""Test that all query patterns route to their optimal tables"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
# Mock empty results for all queries
|
||||
mock_tg_instance.get_all.return_value = []
|
||||
|
|
@ -655,9 +723,9 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value=s, is_uri=False) if s else None,
|
||||
p=Value(value=p, is_uri=False) if p else None,
|
||||
o=Value(value=o, is_uri=False) if o else None,
|
||||
s=Term(type=LITERAL, value=s) if s else None,
|
||||
p=Term(type=LITERAL, value=p) if p else None,
|
||||
o=Term(type=LITERAL, value=o) if o else None,
|
||||
limit=10
|
||||
)
|
||||
|
||||
|
|
@ -687,19 +755,23 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
# Mode is determined in KnowledgeGraph initialization
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('trustgraph.query.triples.cassandra.service.KnowledgeGraph')
|
||||
async def test_performance_critical_po_query_no_filtering(self, mock_trustgraph):
|
||||
@patch('trustgraph.query.triples.cassandra.service.EntityCentricKnowledgeGraph')
|
||||
async def test_performance_critical_po_query_no_filtering(self, mock_kg_class):
|
||||
"""Test the performance-critical PO query that eliminates ALLOW FILTERING"""
|
||||
from trustgraph.schema import TriplesQueryRequest, Value
|
||||
from trustgraph.schema import TriplesQueryRequest, Term, IRI, LITERAL
|
||||
|
||||
mock_tg_instance = MagicMock()
|
||||
mock_trustgraph.return_value = mock_tg_instance
|
||||
mock_kg_class.return_value = mock_tg_instance
|
||||
|
||||
# Mock multiple subjects for the same predicate-object pair
|
||||
mock_results = []
|
||||
for i in range(5):
|
||||
mock_result = MagicMock()
|
||||
mock_result.s = f'subject_{i}'
|
||||
mock_result.g = ''
|
||||
mock_result.otype = None
|
||||
mock_result.dtype = None
|
||||
mock_result.lang = None
|
||||
mock_results.append(mock_result)
|
||||
|
||||
mock_tg_instance.get_po.return_value = mock_results
|
||||
|
|
@ -711,8 +783,8 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
user='large_dataset_user',
|
||||
collection='massive_collection',
|
||||
s=None,
|
||||
p=Value(value='http://www.w3.org/1999/02/22-rdf-syntax-ns#type', is_uri=True),
|
||||
o=Value(value='http://example.com/Person', is_uri=True),
|
||||
p=Term(type=IRI, iri='http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
|
||||
o=Term(type=IRI, iri='http://example.com/Person'),
|
||||
limit=1000
|
||||
)
|
||||
|
||||
|
|
@ -723,14 +795,15 @@ class TestCassandraQueryPerformanceOptimizations:
|
|||
'massive_collection',
|
||||
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type',
|
||||
'http://example.com/Person',
|
||||
g=None,
|
||||
limit=1000
|
||||
)
|
||||
|
||||
# Verify all results were returned
|
||||
assert len(result) == 5
|
||||
for i, triple in enumerate(result):
|
||||
assert triple.s.value == f'subject_{i}'
|
||||
assert triple.p.value == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
|
||||
assert triple.p.is_uri is True
|
||||
assert triple.o.value == 'http://example.com/Person'
|
||||
assert triple.o.is_uri is True
|
||||
assert triple.s.value == f'subject_{i}' # Mock returns literal values
|
||||
assert triple.p.iri == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
|
||||
assert triple.p.type == IRI
|
||||
assert triple.o.iri == 'http://example.com/Person' # URIs use .iri
|
||||
assert triple.o.type == IRI
|
||||
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from trustgraph.query.triples.falkordb.service import Processor
|
||||
from trustgraph.schema import Value, TriplesQueryRequest
|
||||
from trustgraph.schema import Term, TriplesQueryRequest, IRI, LITERAL
|
||||
|
||||
|
||||
class TestFalkorDBQueryProcessor:
|
||||
|
|
@ -25,50 +25,50 @@ class TestFalkorDBQueryProcessor:
|
|||
def test_create_value_with_http_uri(self, processor):
|
||||
"""Test create_value with HTTP URI"""
|
||||
result = processor.create_value("http://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "http://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "http://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_value_with_https_uri(self, processor):
|
||||
"""Test create_value with HTTPS URI"""
|
||||
result = processor.create_value("https://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "https://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "https://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_value_with_literal(self, processor):
|
||||
"""Test create_value with literal value"""
|
||||
result = processor.create_value("just a literal string")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "just a literal string"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_empty_string(self, processor):
|
||||
"""Test create_value with empty string"""
|
||||
result = processor.create_value("")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == ""
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_partial_uri(self, processor):
|
||||
"""Test create_value with string that looks like URI but isn't complete"""
|
||||
result = processor.create_value("http")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "http"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_ftp_uri(self, processor):
|
||||
"""Test create_value with FTP URI (should not be detected as URI)"""
|
||||
result = processor.create_value("ftp://example.com/file")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "ftp://example.com/file"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
@patch('trustgraph.query.triples.falkordb.service.FalkorDB')
|
||||
def test_processor_initialization_with_defaults(self, mock_falkordb):
|
||||
|
|
@ -125,9 +125,9 @@ class TestFalkorDBQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
o=Value(value="literal object", is_uri=False),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=Term(type=LITERAL, value="literal object"),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
|
@ -138,8 +138,8 @@ class TestFalkorDBQueryProcessor:
|
|||
|
||||
# Verify result contains the queried triple (appears twice - once from each query)
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal object"
|
||||
|
||||
@patch('trustgraph.query.triples.falkordb.service.FalkorDB')
|
||||
|
|
@ -166,8 +166,8 @@ class TestFalkorDBQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=None,
|
||||
limit=100
|
||||
)
|
||||
|
|
@ -179,13 +179,13 @@ class TestFalkorDBQueryProcessor:
|
|||
|
||||
# Verify results contain different objects
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal result"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subject"
|
||||
assert result[1].p.value == "http://example.com/predicate"
|
||||
assert result[1].o.value == "http://example.com/uri_result"
|
||||
assert result[1].s.iri == "http://example.com/subject"
|
||||
assert result[1].p.iri == "http://example.com/predicate"
|
||||
assert result[1].o.iri == "http://example.com/uri_result"
|
||||
|
||||
@patch('trustgraph.query.triples.falkordb.service.FalkorDB')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -211,9 +211,9 @@ class TestFalkorDBQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=None,
|
||||
o=Value(value="literal object", is_uri=False),
|
||||
o=Term(type=LITERAL, value="literal object"),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
|
@ -224,12 +224,12 @@ class TestFalkorDBQueryProcessor:
|
|||
|
||||
# Verify results contain different predicates
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/pred1"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/pred1"
|
||||
assert result[0].o.value == "literal object"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subject"
|
||||
assert result[1].p.value == "http://example.com/pred2"
|
||||
assert result[1].s.iri == "http://example.com/subject"
|
||||
assert result[1].p.iri == "http://example.com/pred2"
|
||||
assert result[1].o.value == "literal object"
|
||||
|
||||
@patch('trustgraph.query.triples.falkordb.service.FalkorDB')
|
||||
|
|
@ -256,7 +256,7 @@ class TestFalkorDBQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=100
|
||||
|
|
@ -269,13 +269,13 @@ class TestFalkorDBQueryProcessor:
|
|||
|
||||
# Verify results contain different predicate-object pairs
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/pred1"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/pred1"
|
||||
assert result[0].o.value == "literal1"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subject"
|
||||
assert result[1].p.value == "http://example.com/pred2"
|
||||
assert result[1].o.value == "http://example.com/uri2"
|
||||
assert result[1].s.iri == "http://example.com/subject"
|
||||
assert result[1].p.iri == "http://example.com/pred2"
|
||||
assert result[1].o.iri == "http://example.com/uri2"
|
||||
|
||||
@patch('trustgraph.query.triples.falkordb.service.FalkorDB')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -302,8 +302,8 @@ class TestFalkorDBQueryProcessor:
|
|||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=None,
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
o=Value(value="literal object", is_uri=False),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=Term(type=LITERAL, value="literal object"),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
|
@ -314,12 +314,12 @@ class TestFalkorDBQueryProcessor:
|
|||
|
||||
# Verify results contain different subjects
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subj1"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subj1"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal object"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subj2"
|
||||
assert result[1].p.value == "http://example.com/predicate"
|
||||
assert result[1].s.iri == "http://example.com/subj2"
|
||||
assert result[1].p.iri == "http://example.com/predicate"
|
||||
assert result[1].o.value == "literal object"
|
||||
|
||||
@patch('trustgraph.query.triples.falkordb.service.FalkorDB')
|
||||
|
|
@ -347,7 +347,7 @@ class TestFalkorDBQueryProcessor:
|
|||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=None,
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=None,
|
||||
limit=100
|
||||
)
|
||||
|
|
@ -359,13 +359,13 @@ class TestFalkorDBQueryProcessor:
|
|||
|
||||
# Verify results contain different subject-object pairs
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subj1"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subj1"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal1"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subj2"
|
||||
assert result[1].p.value == "http://example.com/predicate"
|
||||
assert result[1].o.value == "http://example.com/uri2"
|
||||
assert result[1].s.iri == "http://example.com/subj2"
|
||||
assert result[1].p.iri == "http://example.com/predicate"
|
||||
assert result[1].o.iri == "http://example.com/uri2"
|
||||
|
||||
@patch('trustgraph.query.triples.falkordb.service.FalkorDB')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -393,7 +393,7 @@ class TestFalkorDBQueryProcessor:
|
|||
collection='test_collection',
|
||||
s=None,
|
||||
p=None,
|
||||
o=Value(value="literal object", is_uri=False),
|
||||
o=Term(type=LITERAL, value="literal object"),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
|
@ -404,12 +404,12 @@ class TestFalkorDBQueryProcessor:
|
|||
|
||||
# Verify results contain different subject-predicate pairs
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subj1"
|
||||
assert result[0].p.value == "http://example.com/pred1"
|
||||
assert result[0].s.iri == "http://example.com/subj1"
|
||||
assert result[0].p.iri == "http://example.com/pred1"
|
||||
assert result[0].o.value == "literal object"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subj2"
|
||||
assert result[1].p.value == "http://example.com/pred2"
|
||||
assert result[1].s.iri == "http://example.com/subj2"
|
||||
assert result[1].p.iri == "http://example.com/pred2"
|
||||
assert result[1].o.value == "literal object"
|
||||
|
||||
@patch('trustgraph.query.triples.falkordb.service.FalkorDB')
|
||||
|
|
@ -449,13 +449,13 @@ class TestFalkorDBQueryProcessor:
|
|||
|
||||
# Verify results contain different triples
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/s1"
|
||||
assert result[0].p.value == "http://example.com/p1"
|
||||
assert result[0].s.iri == "http://example.com/s1"
|
||||
assert result[0].p.iri == "http://example.com/p1"
|
||||
assert result[0].o.value == "literal1"
|
||||
|
||||
assert result[1].s.value == "http://example.com/s2"
|
||||
assert result[1].p.value == "http://example.com/p2"
|
||||
assert result[1].o.value == "http://example.com/o2"
|
||||
assert result[1].s.iri == "http://example.com/s2"
|
||||
assert result[1].p.iri == "http://example.com/p2"
|
||||
assert result[1].o.iri == "http://example.com/o2"
|
||||
|
||||
@patch('trustgraph.query.triples.falkordb.service.FalkorDB')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -476,7 +476,7 @@ class TestFalkorDBQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=100
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from trustgraph.query.triples.memgraph.service import Processor
|
||||
from trustgraph.schema import Value, TriplesQueryRequest
|
||||
from trustgraph.schema import Term, TriplesQueryRequest, IRI, LITERAL
|
||||
|
||||
|
||||
class TestMemgraphQueryProcessor:
|
||||
|
|
@ -25,50 +25,50 @@ class TestMemgraphQueryProcessor:
|
|||
def test_create_value_with_http_uri(self, processor):
|
||||
"""Test create_value with HTTP URI"""
|
||||
result = processor.create_value("http://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "http://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "http://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_value_with_https_uri(self, processor):
|
||||
"""Test create_value with HTTPS URI"""
|
||||
result = processor.create_value("https://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "https://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "https://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_value_with_literal(self, processor):
|
||||
"""Test create_value with literal value"""
|
||||
result = processor.create_value("just a literal string")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "just a literal string"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_empty_string(self, processor):
|
||||
"""Test create_value with empty string"""
|
||||
result = processor.create_value("")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == ""
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_partial_uri(self, processor):
|
||||
"""Test create_value with string that looks like URI but isn't complete"""
|
||||
result = processor.create_value("http")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "http"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_ftp_uri(self, processor):
|
||||
"""Test create_value with FTP URI (should not be detected as URI)"""
|
||||
result = processor.create_value("ftp://example.com/file")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "ftp://example.com/file"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
@patch('trustgraph.query.triples.memgraph.service.GraphDatabase')
|
||||
def test_processor_initialization_with_defaults(self, mock_graph_db):
|
||||
|
|
@ -124,9 +124,9 @@ class TestMemgraphQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
o=Value(value="literal object", is_uri=False),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=Term(type=LITERAL, value="literal object"),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
|
@ -137,8 +137,8 @@ class TestMemgraphQueryProcessor:
|
|||
|
||||
# Verify result contains the queried triple (appears twice - once from each query)
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal object"
|
||||
|
||||
@patch('trustgraph.query.triples.memgraph.service.GraphDatabase')
|
||||
|
|
@ -166,8 +166,8 @@ class TestMemgraphQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=None,
|
||||
limit=100
|
||||
)
|
||||
|
|
@ -179,13 +179,13 @@ class TestMemgraphQueryProcessor:
|
|||
|
||||
# Verify results contain different objects
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal result"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subject"
|
||||
assert result[1].p.value == "http://example.com/predicate"
|
||||
assert result[1].o.value == "http://example.com/uri_result"
|
||||
assert result[1].s.iri == "http://example.com/subject"
|
||||
assert result[1].p.iri == "http://example.com/predicate"
|
||||
assert result[1].o.iri == "http://example.com/uri_result"
|
||||
|
||||
@patch('trustgraph.query.triples.memgraph.service.GraphDatabase')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -212,9 +212,9 @@ class TestMemgraphQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=None,
|
||||
o=Value(value="literal object", is_uri=False),
|
||||
o=Term(type=LITERAL, value="literal object"),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
|
@ -225,12 +225,12 @@ class TestMemgraphQueryProcessor:
|
|||
|
||||
# Verify results contain different predicates
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/pred1"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/pred1"
|
||||
assert result[0].o.value == "literal object"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subject"
|
||||
assert result[1].p.value == "http://example.com/pred2"
|
||||
assert result[1].s.iri == "http://example.com/subject"
|
||||
assert result[1].p.iri == "http://example.com/pred2"
|
||||
assert result[1].o.value == "literal object"
|
||||
|
||||
@patch('trustgraph.query.triples.memgraph.service.GraphDatabase')
|
||||
|
|
@ -258,7 +258,7 @@ class TestMemgraphQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=100
|
||||
|
|
@ -271,13 +271,13 @@ class TestMemgraphQueryProcessor:
|
|||
|
||||
# Verify results contain different predicate-object pairs
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/pred1"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/pred1"
|
||||
assert result[0].o.value == "literal1"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subject"
|
||||
assert result[1].p.value == "http://example.com/pred2"
|
||||
assert result[1].o.value == "http://example.com/uri2"
|
||||
assert result[1].s.iri == "http://example.com/subject"
|
||||
assert result[1].p.iri == "http://example.com/pred2"
|
||||
assert result[1].o.iri == "http://example.com/uri2"
|
||||
|
||||
@patch('trustgraph.query.triples.memgraph.service.GraphDatabase')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -305,8 +305,8 @@ class TestMemgraphQueryProcessor:
|
|||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=None,
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
o=Value(value="literal object", is_uri=False),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=Term(type=LITERAL, value="literal object"),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
|
@ -317,12 +317,12 @@ class TestMemgraphQueryProcessor:
|
|||
|
||||
# Verify results contain different subjects
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subj1"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subj1"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal object"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subj2"
|
||||
assert result[1].p.value == "http://example.com/predicate"
|
||||
assert result[1].s.iri == "http://example.com/subj2"
|
||||
assert result[1].p.iri == "http://example.com/predicate"
|
||||
assert result[1].o.value == "literal object"
|
||||
|
||||
@patch('trustgraph.query.triples.memgraph.service.GraphDatabase')
|
||||
|
|
@ -351,7 +351,7 @@ class TestMemgraphQueryProcessor:
|
|||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=None,
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=None,
|
||||
limit=100
|
||||
)
|
||||
|
|
@ -363,13 +363,13 @@ class TestMemgraphQueryProcessor:
|
|||
|
||||
# Verify results contain different subject-object pairs
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subj1"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subj1"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal1"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subj2"
|
||||
assert result[1].p.value == "http://example.com/predicate"
|
||||
assert result[1].o.value == "http://example.com/uri2"
|
||||
assert result[1].s.iri == "http://example.com/subj2"
|
||||
assert result[1].p.iri == "http://example.com/predicate"
|
||||
assert result[1].o.iri == "http://example.com/uri2"
|
||||
|
||||
@patch('trustgraph.query.triples.memgraph.service.GraphDatabase')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -398,7 +398,7 @@ class TestMemgraphQueryProcessor:
|
|||
collection='test_collection',
|
||||
s=None,
|
||||
p=None,
|
||||
o=Value(value="literal object", is_uri=False),
|
||||
o=Term(type=LITERAL, value="literal object"),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
|
@ -409,12 +409,12 @@ class TestMemgraphQueryProcessor:
|
|||
|
||||
# Verify results contain different subject-predicate pairs
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subj1"
|
||||
assert result[0].p.value == "http://example.com/pred1"
|
||||
assert result[0].s.iri == "http://example.com/subj1"
|
||||
assert result[0].p.iri == "http://example.com/pred1"
|
||||
assert result[0].o.value == "literal object"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subj2"
|
||||
assert result[1].p.value == "http://example.com/pred2"
|
||||
assert result[1].s.iri == "http://example.com/subj2"
|
||||
assert result[1].p.iri == "http://example.com/pred2"
|
||||
assert result[1].o.value == "literal object"
|
||||
|
||||
@patch('trustgraph.query.triples.memgraph.service.GraphDatabase')
|
||||
|
|
@ -455,13 +455,13 @@ class TestMemgraphQueryProcessor:
|
|||
|
||||
# Verify results contain different triples
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/s1"
|
||||
assert result[0].p.value == "http://example.com/p1"
|
||||
assert result[0].s.iri == "http://example.com/s1"
|
||||
assert result[0].p.iri == "http://example.com/p1"
|
||||
assert result[0].o.value == "literal1"
|
||||
|
||||
assert result[1].s.value == "http://example.com/s2"
|
||||
assert result[1].p.value == "http://example.com/p2"
|
||||
assert result[1].o.value == "http://example.com/o2"
|
||||
assert result[1].s.iri == "http://example.com/s2"
|
||||
assert result[1].p.iri == "http://example.com/p2"
|
||||
assert result[1].o.iri == "http://example.com/o2"
|
||||
|
||||
@patch('trustgraph.query.triples.memgraph.service.GraphDatabase')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -480,7 +480,7 @@ class TestMemgraphQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=100
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from trustgraph.query.triples.neo4j.service import Processor
|
||||
from trustgraph.schema import Value, TriplesQueryRequest
|
||||
from trustgraph.schema import Term, TriplesQueryRequest, IRI, LITERAL
|
||||
|
||||
|
||||
class TestNeo4jQueryProcessor:
|
||||
|
|
@ -25,50 +25,50 @@ class TestNeo4jQueryProcessor:
|
|||
def test_create_value_with_http_uri(self, processor):
|
||||
"""Test create_value with HTTP URI"""
|
||||
result = processor.create_value("http://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "http://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "http://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_value_with_https_uri(self, processor):
|
||||
"""Test create_value with HTTPS URI"""
|
||||
result = processor.create_value("https://example.com/resource")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "https://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.iri == "https://example.com/resource"
|
||||
assert result.type == IRI
|
||||
|
||||
def test_create_value_with_literal(self, processor):
|
||||
"""Test create_value with literal value"""
|
||||
result = processor.create_value("just a literal string")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "just a literal string"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_empty_string(self, processor):
|
||||
"""Test create_value with empty string"""
|
||||
result = processor.create_value("")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == ""
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_partial_uri(self, processor):
|
||||
"""Test create_value with string that looks like URI but isn't complete"""
|
||||
result = processor.create_value("http")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "http"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
def test_create_value_with_ftp_uri(self, processor):
|
||||
"""Test create_value with FTP URI (should not be detected as URI)"""
|
||||
result = processor.create_value("ftp://example.com/file")
|
||||
|
||||
assert isinstance(result, Value)
|
||||
|
||||
assert isinstance(result, Term)
|
||||
assert result.value == "ftp://example.com/file"
|
||||
assert result.is_uri is False
|
||||
assert result.type == LITERAL
|
||||
|
||||
@patch('trustgraph.query.triples.neo4j.service.GraphDatabase')
|
||||
def test_processor_initialization_with_defaults(self, mock_graph_db):
|
||||
|
|
@ -124,9 +124,9 @@ class TestNeo4jQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
o=Value(value="literal object", is_uri=False),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=Term(type=LITERAL, value="literal object"),
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
|
@ -137,8 +137,8 @@ class TestNeo4jQueryProcessor:
|
|||
|
||||
# Verify result contains the queried triple (appears twice - once from each query)
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal object"
|
||||
|
||||
@patch('trustgraph.query.triples.neo4j.service.GraphDatabase')
|
||||
|
|
@ -166,8 +166,8 @@ class TestNeo4jQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
p=Value(value="http://example.com/predicate", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=Term(type=IRI, iri="http://example.com/predicate"),
|
||||
o=None,
|
||||
limit=100
|
||||
)
|
||||
|
|
@ -179,13 +179,13 @@ class TestNeo4jQueryProcessor:
|
|||
|
||||
# Verify results contain different objects
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/subject"
|
||||
assert result[0].p.value == "http://example.com/predicate"
|
||||
assert result[0].s.iri == "http://example.com/subject"
|
||||
assert result[0].p.iri == "http://example.com/predicate"
|
||||
assert result[0].o.value == "literal result"
|
||||
|
||||
assert result[1].s.value == "http://example.com/subject"
|
||||
assert result[1].p.value == "http://example.com/predicate"
|
||||
assert result[1].o.value == "http://example.com/uri_result"
|
||||
|
||||
assert result[1].s.iri == "http://example.com/subject"
|
||||
assert result[1].p.iri == "http://example.com/predicate"
|
||||
assert result[1].o.iri == "http://example.com/uri_result"
|
||||
|
||||
@patch('trustgraph.query.triples.neo4j.service.GraphDatabase')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -225,13 +225,13 @@ class TestNeo4jQueryProcessor:
|
|||
|
||||
# Verify results contain different triples
|
||||
assert len(result) == 2
|
||||
assert result[0].s.value == "http://example.com/s1"
|
||||
assert result[0].p.value == "http://example.com/p1"
|
||||
assert result[0].s.iri == "http://example.com/s1"
|
||||
assert result[0].p.iri == "http://example.com/p1"
|
||||
assert result[0].o.value == "literal1"
|
||||
|
||||
assert result[1].s.value == "http://example.com/s2"
|
||||
assert result[1].p.value == "http://example.com/p2"
|
||||
assert result[1].o.value == "http://example.com/o2"
|
||||
|
||||
assert result[1].s.iri == "http://example.com/s2"
|
||||
assert result[1].p.iri == "http://example.com/p2"
|
||||
assert result[1].o.iri == "http://example.com/o2"
|
||||
|
||||
@patch('trustgraph.query.triples.neo4j.service.GraphDatabase')
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -250,12 +250,12 @@ class TestNeo4jQueryProcessor:
|
|||
query = TriplesQueryRequest(
|
||||
user='test_user',
|
||||
collection='test_collection',
|
||||
s=Value(value="http://example.com/subject", is_uri=True),
|
||||
s=Term(type=IRI, iri="http://example.com/subject"),
|
||||
p=None,
|
||||
o=None,
|
||||
limit=100
|
||||
)
|
||||
|
||||
|
||||
# Should raise the exception
|
||||
with pytest.raises(Exception, match="Database connection failed"):
|
||||
await processor.query_triples(query)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue