From 329b3abf12223b94be54ff4915f6d16fd1dc228c Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 25 Jun 2026 01:57:08 +0200 Subject: [PATCH 1/2] Make the embedded test suite CI-ready - Fix stale test_match_1 (framework_version default is now empty/unenforced) - Make the external-server provider tests cwd-independent and self-contained - Generate the secured test's certificate in-process (tests/certificates.py) - Add pin_framework_version helper so CI can pin the server's .NET runtime --- tests/__init__.py | 12 ++++ tests/certificates.py | 69 +++++++++++++++++++ tests/test_basic.py | 4 +- tests/test_custom_provider.py | 14 +++- .../test_runtime_framework_version_matcher.py | 22 ++---- tests/test_secured_basic.py | 15 ++-- 6 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 tests/certificates.py diff --git a/tests/__init__.py b/tests/__init__.py index a9e5908..81176b6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,16 @@ +import os + + class Person: def __init__(self, Id: str = None, name: str = None): self.Id = Id self.name = name + + +def pin_framework_version(server_options): + # CI's .NET-version matrix sets RAVENDB_TEST_FRAMEWORK_VERSION to force the server onto a + # specific runtime (e.g. "10.0.x"); a no-op locally when it's unset. + framework_version = os.environ.get("RAVENDB_TEST_FRAMEWORK_VERSION") + if framework_version: + server_options.framework_version = framework_version + return server_options diff --git a/tests/certificates.py b/tests/certificates.py new file mode 100644 index 0000000..ef5c3d0 --- /dev/null +++ b/tests/certificates.py @@ -0,0 +1,69 @@ +import datetime +import ipaddress +from pathlib import Path + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.serialization import pkcs12 +from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID + + +def generate_self_signed_certificates(directory): + """Write server.pfx, client.pem and ca.crt into `directory`; return their paths. + + The certificate carries the extensions RavenDB requires of a server certificate: + DigitalSignature key usage, serverAuth/clientAuth EKU, and SANs for how the embedded + server is reached (localhost + 127.0.0.1). The same cert doubles as the client cert, + which the server then trusts as a well-known admin certificate. + """ + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "localhost")]) + now = datetime.datetime.now(datetime.timezone.utc) + certificate = ( + x509.CertificateBuilder() + .subject_name(name) + .issuer_name(name) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now - datetime.timedelta(days=1)) + .not_valid_after(now + datetime.timedelta(days=3650)) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("localhost"), x509.IPAddress(ipaddress.ip_address("127.0.0.1"))]), + critical=False, + ) + .add_extension( + x509.KeyUsage( + digital_signature=True, + key_encipherment=True, + content_commitment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH, ExtendedKeyUsageOID.CLIENT_AUTH]), + critical=False, + ) + .sign(key, hashes.SHA256()) + ) + + certificate_pem = certificate.public_bytes(serialization.Encoding.PEM) + key_pem = key.private_bytes( + serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption() + ) + + server_pfx = Path(directory, "server.pfx") + client_pem = Path(directory, "client.pem") + ca_crt = Path(directory, "ca.crt") + server_pfx.write_bytes( + pkcs12.serialize_key_and_certificates(b"localhost", key, certificate, None, serialization.NoEncryption()) + ) + client_pem.write_bytes(key_pem + certificate_pem) + ca_crt.write_bytes(certificate_pem) + return str(server_pfx), str(client_pem), str(ca_crt) diff --git a/tests/test_basic.py b/tests/test_basic.py index 526eb08..b3c1f86 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -4,7 +4,7 @@ from unittest import TestCase from ravendb_embedded import EmbeddedServer, ServerOptions, CopyServerFromNugetProvider, DatabaseOptions -from tests import Person +from tests import Person, pin_framework_version class BasicTest(TestCase): @@ -17,6 +17,7 @@ def test_embedded(self): server_options.logs_path = str(Path(temp_dir, "Logs")) server_options.provider = CopyServerFromNugetProvider() server_options.command_line_args = ["--Features.Availability=Experimental"] + pin_framework_version(server_options) embedded.start_server(server_options) database_options = DatabaseOptions.from_database_name("Test") @@ -37,6 +38,7 @@ def test_embedded(self): server_options = ServerOptions() server_options.data_directory = str(Path(temp_dir, "RavenDB")) server_options.provider = CopyServerFromNugetProvider() + pin_framework_version(server_options) embedded.start_server(server_options) with embedded.get_document_store("Test") as store: diff --git a/tests/test_custom_provider.py b/tests/test_custom_provider.py index 55fcd9a..9880a5d 100644 --- a/tests/test_custom_provider.py +++ b/tests/test_custom_provider.py @@ -1,3 +1,4 @@ +import shutil import tempfile from pathlib import Path from unittest import TestCase @@ -5,7 +6,7 @@ from ravendb_embedded.embedded_server import EmbeddedServer from ravendb_embedded.options import ServerOptions, DatabaseOptions from ravendb_embedded.provide import CopyServerFromNugetProvider -from tests import Person +from tests import Person, pin_framework_version class TestCustomProvider(TestCase): @@ -15,14 +16,21 @@ def configure_server_options(temp_dir: str, server_options: ServerOptions) -> Se server_options.data_directory = str(Path(temp_dir, "RavenDB")) server_options.logs_path = str(Path(temp_dir, "Logs")) server_options.command_line_args = ["--Features.Availability=Experimental"] + pin_framework_version(server_options) return server_options def test_can_use_zip_as_external_server_source(self): with tempfile.TemporaryDirectory() as temp_dir: + # Zip the bundled server so the test does not depend on cwd or a pre-built archive. + server_zip = shutil.make_archive( + str(Path(temp_dir, "ravendb-server")), + "zip", + CopyServerFromNugetProvider().server_files, + ) with EmbeddedServer() as embedded: server_options = ServerOptions() server_options = self.configure_server_options(temp_dir, server_options) - server_options.with_external_server("ravendb_embedded/target/ravendb-server.zip") + server_options.with_external_server(server_zip) embedded.start_server(server_options) database_options = DatabaseOptions.from_database_name("Test") @@ -37,7 +45,7 @@ def test_can_use_directory_as_external_server_source(self): with EmbeddedServer() as embedded: server_options = ServerOptions() server_options = self.configure_server_options(temp_directory, server_options) - server_options.with_external_server(CopyServerFromNugetProvider.SERVER_FILES) + server_options.with_external_server(CopyServerFromNugetProvider().server_files) embedded.start_server(server_options) database_options = DatabaseOptions.from_database_name("Test") diff --git a/tests/test_runtime_framework_version_matcher.py b/tests/test_runtime_framework_version_matcher.py index c6fde32..d0f0a58 100644 --- a/tests/test_runtime_framework_version_matcher.py +++ b/tests/test_runtime_framework_version_matcher.py @@ -9,28 +9,16 @@ class TestRuntimeFrameworkVersionMatcher(TestCase): def test_match_1(self): - options = ServerOptions() - default_framework_version = ServerOptions.INSTANCE().framework_version + # Default framework version is unenforced (empty); match() returns it unchanged. + self.assertFalse(ServerOptions.INSTANCE().framework_version) - self.assertIn(RuntimeFrameworkVersionMatcher.GREATER_OR_EQUAL, default_framework_version) + options = ServerOptions() options.framework_version = None self.assertIsNone(RuntimeFrameworkVersionMatcher.match(options)) - options = ServerOptions() - framework_version = RuntimeFrameworkVersion(options.framework_version) - framework_version.patch = None - - options.framework_version = framework_version.__str__() - match = RuntimeFrameworkVersionMatcher.match(options) - self.assertIsNotNone(match) - - match_framework_version = RuntimeFrameworkVersion(match) - self.assertIsNotNone(match_framework_version.major) - self.assertIsNotNone(match_framework_version.minor) - self.assertIsNotNone(match_framework_version.patch) - - self.assertTrue(framework_version.match(match_framework_version)) + options.framework_version = "" + self.assertEqual("", RuntimeFrameworkVersionMatcher.match(options)) def test_match_2(self): runtimes = self.get_runtimes() diff --git a/tests/test_secured_basic.py b/tests/test_secured_basic.py index ab83b39..72feebc 100644 --- a/tests/test_secured_basic.py +++ b/tests/test_secured_basic.py @@ -6,27 +6,22 @@ from ravendb_embedded.embedded_server import EmbeddedServer from ravendb_embedded.options import ServerOptions, DatabaseOptions from ravendb_embedded.provide import CopyServerFromNugetProvider -from tests import Person +from tests import Person, pin_framework_version +from tests.certificates import generate_self_signed_certificates class TestSecuredBasic(TestCase): def test_secured_embedded(self): - SERVER_CERTIFICATE_LOCATION = "C:\\RavenDB Clients\\Https\\server.pfx" - CA_CERTIFICATE_LOCATION = "C:\\RavenDB Clients\\Https\\ca.crt" - CLIENT_CERTIFICATE_LOCATION = "C:\\RavenDB Clients\\Https\\python.pem" temp_dir = tempfile.mkdtemp() try: + server_pfx, client_pem, ca_crt = generate_self_signed_certificates(temp_dir) with EmbeddedServer() as embedded: server_options = ServerOptions() - server_options.secured( - SERVER_CERTIFICATE_LOCATION, - CLIENT_CERTIFICATE_LOCATION, - ca_certificate_path=CA_CERTIFICATE_LOCATION, - ) - + server_options.secured(server_pfx, client_pem, ca_certificate_path=ca_crt) server_options.data_directory = str(Path(temp_dir, "RavenDB")) server_options.logs_path = str(Path(temp_dir, "Logs")) server_options.provider = CopyServerFromNugetProvider() + pin_framework_version(server_options) embedded.start_server(server_options) database_options = DatabaseOptions.from_database_name("Test") From beed97c54b08b0881767a4581ce9be05456d8a0b Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 25 Jun 2026 01:57:08 +0200 Subject: [PATCH 2/2] Add GitHub Actions CI for v7.2 (tests + black) - Runs 'unittest discover' + 'black --check' on push/PR to v7.2 - Matrix: Python {3.9,3.13} x OS {ubuntu,windows} x .NET {8 default, 10 forced} - Fetches the bundled RavenDB server via setup.py sdist; black line-length 120 via pyproject.toml --- .github/workflows/tests.yml | 59 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 ++ 2 files changed, 61 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 pyproject.toml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ee9ccd4 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,59 @@ +name: tests + +on: + push: + branches: [v7.2] + pull_request: + branches: [v7.2] + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + env: + # The server targets net8.0, so default resolution already exercises .NET 8. Runners + # preinstall .NET 8, so to actually exercise .NET 10 we force it via fx-version (empty + # = default resolution). See tests/pin_framework_version. + RAVENDB_TEST_FRAMEWORK_VERSION: "${{ matrix.fx }}" + strategy: + fail-fast: false + # Minimal spread: Python {3.9,3.13} x OS {ubuntu,windows} x .NET {8 default, 10 forced} + # each appears at least once. + matrix: + include: + - { os: ubuntu-latest, python: "3.13", dotnet: "10.0", fx: "10.0.x" } + - { os: ubuntu-latest, python: "3.9", dotnet: "8.0", fx: "" } + - { os: windows-latest, python: "3.13", dotnet: "10.0", fx: "10.0.x" } + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Set up .NET ${{ matrix.dotnet }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet }} + + - name: Install package + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Check formatting (black) + run: | + pip install black + black --check . + + # Populates ravendb_embedded/target/nuget via the project's sdist hook (the server the + # tests copy from). setuptools is installed explicitly; modern virtualenvs omit it. + - name: Fetch bundled RavenDB server + run: | + pip install setuptools wheel + python setup.py sdist + + - name: Run tests + run: python -m unittest discover -s tests diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..55ec8d7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 120