Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tool.black]
line-length = 120
12 changes: 12 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions tests/certificates.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 3 additions & 1 deletion tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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")
Expand All @@ -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:
Expand Down
14 changes: 11 additions & 3 deletions tests/test_custom_provider.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import shutil
import tempfile
from pathlib import Path
from unittest import TestCase

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):
Expand All @@ -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")
Expand All @@ -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")
Expand Down
22 changes: 5 additions & 17 deletions tests/test_runtime_framework_version_matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
15 changes: 5 additions & 10 deletions tests/test_secured_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down