9 Commits

Author SHA1 Message Date
dextmorgn
12bf2937c1 Merge pull request #176 from reconurge/ci/python-tests
ci: run python tests on pull requests and main
2026-06-05 15:47:32 +02:00
dextmorgn
612d39eadf test: provide dummy Neo4j credentials in conftest
Any code path reaching the Neo4jConnection singleton requires NEO4J_*
env vars at construction — tests only passed thanks to developers'
local .env files. Driver creation is lazy, nothing connects.

Verified by running all four suites with every .env removed:
458 passed.
2026-06-05 15:34:58 +02:00
dextmorgn
dc36ef633e test(core): factor out env-independent no-connection repo setup
Previous fix covered one occurrence; 17 other tests built
Neo4jGraphRepository(neo4j_connection=None), hitting the singleton
fallback that requires NEO4J_* env vars. Single helper, no env
dependency left.
2026-06-05 15:26:08 +02:00
dextmorgn
22c6908a0e test(core): stop relying on env credentials in no-connection test
Neo4jGraphRepository(neo4j_connection=None) falls back to the
Neo4jConnection singleton, which requires NEO4J_* env vars — the test
only passed locally thanks to .env. Pass a mock instead; the test
nulls _connection right after anyway.
2026-06-05 15:17:37 +02:00
dextmorgn
71e3a68830 fix(core): declare pyyaml dependency
flowsint_core imports yaml (template_generator_service, yaml_loader)
but never declared it — present locally only as a leftover in the
shared venv. CI's clean resolve exposed the missing declaration.
2026-06-05 15:10:18 +02:00
dextmorgn
a24551ed55 ci: provide REDIS_URL, also import-time required by flowsint_core 2026-06-05 14:43:37 +02:00
dextmorgn
17e36c9162 ci: provide AUTH_SECRET required at flowsint_core import 2026-06-05 14:32:12 +02:00
dextmorgn
07584ebc4a ci: run python tests on pull requests and main
Single entrypoint: CI calls `make test`, same as local development,
so the two can't drift. uv handles Python (.python-version) and
dependency sync; cache keyed on uv.lock.

Also add flowsint-api to `make test` — its suite existed but was
never wired in.
2026-06-05 14:24:34 +02:00
dextmorgn
db9f50d914 Merge pull request #175 from reconurge/fix/prebuilt-images-compose
fix(deploy): serve prod from pre-built images
2026-06-05 14:11:29 +02:00
7 changed files with 89 additions and 36 deletions

37
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Tests
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Python tests
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
# Same entrypoint as local development — keeps CI and `make test` in sync.
- name: Run tests
run: make test
env:
# flowsint_core hard-requires these at import time (provided by .env
# locally). Dummy values — no service is reached during tests:
# connections (redis.from_url, create_engine) are lazy.
AUTH_SECRET: ci-only-dummy-secret
REDIS_URL: redis://127.0.0.1:6379/0

View File

@@ -140,6 +140,7 @@ test:
cd flowsint-types && uv run pytest
cd flowsint-core && uv run pytest
cd flowsint-enrichers && uv run pytest
cd flowsint-api && uv run pytest
install:
$(MAKE) infra-dev

View File

@@ -14,6 +14,7 @@ dependencies = [
"redis>=5.0,<6.0",
"celery>=5.3,<6.0",
"python-dotenv>=1.0,<2.0",
"pyyaml>=6.0,<7.0",
"requests>=2.31,<3.0",
"httpx>=0.28,<0.29",
"networkx>=2.6.3,<3.0.0",

View File

@@ -17,6 +17,12 @@ def setup_test_environment(monkeypatch):
# Set a test master key for vault tests
test_key = "base64:qnHTmwYb+uoygIw9MsRMY22vS5YPchY+QOi/E79GAvM="
monkeypatch.setenv("MASTER_VAULT_KEY_V1", test_key)
# Dummy Neo4j credentials: the Neo4jConnection singleton requires them
# at construction, but driver creation is lazy — nothing connects.
# Without these, tests silently depend on the developer's local .env.
monkeypatch.setenv("NEO4J_URI_BOLT", "bolt://127.0.0.1:7687")
monkeypatch.setenv("NEO4J_USERNAME", "neo4j")
monkeypatch.setenv("NEO4J_PASSWORD", "test-password")
@pytest.fixture(autouse=True)

View File

@@ -7,6 +7,17 @@ from datetime import datetime, timezone
from flowsint_core.core.graph import Neo4jGraphRepository
def repo_without_connection() -> Neo4jGraphRepository:
"""Repository with no underlying connection.
Constructed with a mock to avoid the constructor's singleton fallback,
which requires NEO4J_* credentials from the environment.
"""
repo = Neo4jGraphRepository(neo4j_connection=MagicMock())
repo._connection = None
return repo
class TestNeo4jGraphRepositoryInit:
def test_init_with_connection(self):
mock_connection = MagicMock()
@@ -43,8 +54,7 @@ class TestCreateNode:
mock_connection.query.assert_called_once()
def test_create_node_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.create_node({"nodeLabel": "test", "nodeType": "domain"}, "sketch-1")
@@ -80,8 +90,7 @@ class TestCreateRelationship:
mock_connection.execute_write.assert_called_once()
def test_create_relationship_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
rel_obj = {
"from_type": "domain",
@@ -231,8 +240,7 @@ class TestBatchOperations:
mock_connection.execute_batch.assert_not_called()
def test_flush_batch_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
repo._batch_operations = [("query", {})]
repo.flush_batch()
@@ -285,8 +293,7 @@ class TestBatchCreateNodes:
assert result["errors"] == []
def test_batch_create_nodes_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.batch_create_nodes(
[{"nodeLabel": "test", "nodeType": "domain"}], sketch_id="sketch-1"
@@ -338,8 +345,7 @@ class TestBatchCreateEdges:
assert result["errors"] == []
def test_batch_create_edges_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.batch_create_edges([{}], sketch_id="sketch-1")
@@ -385,8 +391,7 @@ class TestBatchCreateEdgesByElementId:
assert any("Missing required fields" in e for e in result["errors"])
def test_batch_create_edges_by_element_id_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.batch_create_edges_by_element_id([{}], sketch_id="sketch-1")
@@ -409,8 +414,7 @@ class TestUpdateNode:
assert result == "elem-1"
def test_update_node_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.update_node("elem-1", {"nodeLabel": "x"}, "sketch-1")
@@ -428,8 +432,7 @@ class TestDeleteNodes:
assert result == 3
def test_delete_nodes_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.delete_nodes(["id-1"], sketch_id="sketch-1")
@@ -456,8 +459,7 @@ class TestDeleteRelationships:
assert result == 2
def test_delete_relationships_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.delete_relationships(["rel-1"], sketch_id="sketch-1")
@@ -483,8 +485,7 @@ class TestDeleteAllSketchNodes:
assert result == 10
def test_delete_all_sketch_nodes_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.delete_all_sketch_nodes(sketch_id="sketch-1")
@@ -519,8 +520,7 @@ class TestGetSketchGraph:
assert len(result["edges"]) == 1
def test_get_sketch_graph_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.get_sketch_graph(sketch_id="sketch-1")
@@ -553,8 +553,7 @@ class TestUpdateRelationship:
assert result["id"] == "rel-1"
def test_update_relationship_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.update_relationship("rel-1", {}, "sketch-1")
@@ -577,8 +576,7 @@ class TestCreateRelationshipByElementId:
assert result["sketch_id"] == "sketch-1"
def test_create_relationship_by_element_id_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.create_relationship_by_element_id(
"elem-1", "elem-2", "CONNECTS", "sketch-1"
@@ -598,8 +596,7 @@ class TestQuery:
assert result == [{"count": 5}]
def test_query_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.query("MATCH (n) RETURN n", {})
@@ -622,8 +619,7 @@ class TestUpdateNodesPositions:
assert result == 2
def test_update_nodes_positions_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.update_nodes_positions([{"nodeId": "x", "x": 0, "y": 0}], "s")
@@ -652,8 +648,7 @@ class TestGetNodesByIds:
assert len(result) == 2
def test_get_nodes_by_ids_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.get_nodes_by_ids(["id-1"], sketch_id="sketch-1")
@@ -706,8 +701,7 @@ class TestMergeNodes:
assert result == "old-1"
def test_merge_nodes_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.merge_nodes(["old-1"], {}, None, "sketch-1")
@@ -764,8 +758,7 @@ class TestGetNeighbors:
assert len(result["edges"]) == 0
def test_get_neighbors_no_connection(self):
repo = Neo4jGraphRepository(neo4j_connection=None)
repo._connection = None
repo = repo_without_connection()
result = repo.get_neighbors("node-1", "sketch-1")

View File

@@ -2,6 +2,19 @@ import pytest
from tests.logger import TestLogger
@pytest.fixture(autouse=True)
def setup_test_environment(monkeypatch):
"""Set up test environment variables.
Dummy Neo4j credentials: the Neo4jConnection singleton requires them
at construction, but driver creation is lazy — nothing connects.
Without these, tests silently depend on the developer's local .env.
"""
monkeypatch.setenv("NEO4J_URI_BOLT", "bolt://127.0.0.1:7687")
monkeypatch.setenv("NEO4J_USERNAME", "neo4j")
monkeypatch.setenv("NEO4J_PASSWORD", "test-password")
@pytest.fixture(autouse=True)
def mock_logger(monkeypatch):
"""Automatically replace the production Logger with TestLogger for all tests."""

2
uv.lock generated
View File

@@ -1205,6 +1205,7 @@ dependencies = [
{ name = "python-dotenv" },
{ name = "python-jose", extra = ["cryptography"] },
{ name = "python-multipart" },
{ name = "pyyaml" },
{ name = "redis" },
{ name = "requests" },
{ name = "sqlalchemy" },
@@ -1243,6 +1244,7 @@ requires-dist = [
{ name = "python-dotenv", specifier = ">=1.0,<2.0" },
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.3,<4.0" },
{ name = "python-multipart", specifier = ">=0.0.27,<0.1.0" },
{ name = "pyyaml", specifier = ">=6.0,<7.0" },
{ name = "redis", specifier = ">=5.0,<6.0" },
{ name = "requests", specifier = ">=2.31,<3.0" },
{ name = "sqlalchemy", specifier = ">=2.0,<3.0" },