From 07584ebc4a4029f720b8c7286043cc963ff8f912 Mon Sep 17 00:00:00 2001 From: dextmorgn <64375473+dextmorgn@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:24:34 +0200 Subject: [PATCH 1/7] ci: run python tests on pull requests and main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/tests.yml | 31 +++++++++++++++++++++++++++++++ Makefile | 1 + 2 files changed, 32 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..d90142ea --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,31 @@ +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 diff --git a/Makefile b/Makefile index 9f96966c..770d5d8d 100644 --- a/Makefile +++ b/Makefile @@ -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 From 17e36c916279e9ed81e59ff5ff12892645b1005f Mon Sep 17 00:00:00 2001 From: dextmorgn <64375473+dextmorgn@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:32:12 +0200 Subject: [PATCH 2/7] ci: provide AUTH_SECRET required at flowsint_core import --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d90142ea..681fa68f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,3 +29,7 @@ jobs: # Same entrypoint as local development — keeps CI and `make test` in sync. - name: Run tests run: make test + env: + # flowsint_core requires AUTH_SECRET at import time (provided by + # .env locally). Dummy value — no service is reached during tests. + AUTH_SECRET: ci-only-dummy-secret From a24551ed550829a37db145734c0c3c2f5ed46d7f Mon Sep 17 00:00:00 2001 From: dextmorgn <64375473+dextmorgn@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:43:37 +0200 Subject: [PATCH 3/7] ci: provide REDIS_URL, also import-time required by flowsint_core --- .github/workflows/tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 681fa68f..52750319 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,6 +30,8 @@ jobs: - name: Run tests run: make test env: - # flowsint_core requires AUTH_SECRET at import time (provided by - # .env locally). Dummy value — no service is reached during tests. + # 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 From 71e3a68830af161e2784c8d5e11d282377d6d76a Mon Sep 17 00:00:00 2001 From: dextmorgn <64375473+dextmorgn@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:10:18 +0200 Subject: [PATCH 4/7] fix(core): declare pyyaml dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- flowsint-core/pyproject.toml | 1 + uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/flowsint-core/pyproject.toml b/flowsint-core/pyproject.toml index ff38f61e..f4c5ac28 100644 --- a/flowsint-core/pyproject.toml +++ b/flowsint-core/pyproject.toml @@ -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", diff --git a/uv.lock b/uv.lock index d0d46c08..2651c287 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, From 22c6908a0efb389bb6028af724848164c1d12426 Mon Sep 17 00:00:00 2001 From: dextmorgn <64375473+dextmorgn@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:17:37 +0200 Subject: [PATCH 5/7] test(core): stop relying on env credentials in no-connection test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- flowsint-core/tests/core/graph/test_graph_repository.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flowsint-core/tests/core/graph/test_graph_repository.py b/flowsint-core/tests/core/graph/test_graph_repository.py index 003b553b..5fbcccb5 100644 --- a/flowsint-core/tests/core/graph/test_graph_repository.py +++ b/flowsint-core/tests/core/graph/test_graph_repository.py @@ -43,7 +43,9 @@ class TestCreateNode: mock_connection.query.assert_called_once() def test_create_node_no_connection(self): - repo = Neo4jGraphRepository(neo4j_connection=None) + # Pass a mock to avoid the constructor's singleton fallback, which + # requires NEO4J_* credentials; the test wants no connection at all. + repo = Neo4jGraphRepository(neo4j_connection=MagicMock()) repo._connection = None result = repo.create_node({"nodeLabel": "test", "nodeType": "domain"}, "sketch-1") From dc36ef633e32e088e63e5e2cd51ed2dfbd644e1e Mon Sep 17 00:00:00 2001 From: dextmorgn <64375473+dextmorgn@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:26:08 +0200 Subject: [PATCH 6/7] 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. --- .../tests/core/graph/test_graph_repository.py | 67 ++++++++----------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/flowsint-core/tests/core/graph/test_graph_repository.py b/flowsint-core/tests/core/graph/test_graph_repository.py index 5fbcccb5..9ca97a03 100644 --- a/flowsint-core/tests/core/graph/test_graph_repository.py +++ b/flowsint-core/tests/core/graph/test_graph_repository.py @@ -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,10 +54,7 @@ class TestCreateNode: mock_connection.query.assert_called_once() def test_create_node_no_connection(self): - # Pass a mock to avoid the constructor's singleton fallback, which - # requires NEO4J_* credentials; the test wants no connection at all. - repo = Neo4jGraphRepository(neo4j_connection=MagicMock()) - repo._connection = None + repo = repo_without_connection() result = repo.create_node({"nodeLabel": "test", "nodeType": "domain"}, "sketch-1") @@ -82,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", @@ -233,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() @@ -287,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" @@ -340,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") @@ -387,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") @@ -411,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") @@ -430,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") @@ -458,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") @@ -485,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") @@ -521,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") @@ -555,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") @@ -579,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" @@ -600,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", {}) @@ -624,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") @@ -654,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") @@ -708,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") @@ -766,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") From 612d39eadf72dc3161126d514f806cc1bfb52041 Mon Sep 17 00:00:00 2001 From: dextmorgn <64375473+dextmorgn@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:34:58 +0200 Subject: [PATCH 7/7] test: provide dummy Neo4j credentials in conftest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- flowsint-core/tests/conftest.py | 6 ++++++ flowsint-enrichers/tests/conftest.py | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/flowsint-core/tests/conftest.py b/flowsint-core/tests/conftest.py index bc39b68a..5cd0cfc6 100644 --- a/flowsint-core/tests/conftest.py +++ b/flowsint-core/tests/conftest.py @@ -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) diff --git a/flowsint-enrichers/tests/conftest.py b/flowsint-enrichers/tests/conftest.py index be95bac0..161ca750 100644 --- a/flowsint-enrichers/tests/conftest.py +++ b/flowsint-enrichers/tests/conftest.py @@ -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."""