Skip to content
Open
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
122 changes: 122 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: CI

on:
push:
pull_request:
workflow_dispatch:

jobs:
smoke:
runs-on: ubuntu-latest

services:
mongodb:
image: mongo:latest
options: >-
--health-cmd mongosh
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 27017:27017
env:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: mongodb

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Repo smoke checks
shell: bash
run: |
set -euo pipefail

echo "Validating repository structure and basic runnable signals"

has_signal=0

if find . -maxdepth 4 -type f -name "package.json" | grep -q .; then
has_signal=1
while IFS= read -r pkg; do
[ -z "$pkg" ] && continue
node -e "const fs=require('fs'); JSON.parse(fs.readFileSync(process.argv[1],'utf8'));" "$pkg"
done < <(find . -maxdepth 4 -type f -name "package.json")
fi

if find . -maxdepth 4 -type f \( -name "pyproject.toml" -o -name "requirements.txt" -o -name "setup.py" -o -name "manage.py" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f \( -name "app.py" -o -name "main.py" -o -name "wsgi.py" -o -name "asgi.py" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f \( -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" -o -name "gradlew" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f -name "go.mod" | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f -name "Cargo.toml" | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f \( -name "*.csproj" -o -name "*.sln" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f \( -name "Dockerfile" -o -name "docker-compose.yml" -o -name "docker-compose.yaml" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f -name "Makefile" | grep -q .; then
has_signal=1
fi

if [ "$has_signal" -ne 1 ]; then
echo "No runnable/build signals found in repository"
exit 1
fi

echo "Running Python syntax smoke check"
python_files="$(find . -type f -name '*.py' -not -path './.git/*' 2>/dev/null || true)"
if [ -n "$python_files" ]; then
while IFS= read -r f; do
[ -z "$f" ] && continue
python -m py_compile "$f"
done <<< "$python_files"
fi

echo "Smoke checks passed"

- name: Run repository runtime smoke test
shell: bash
run: |
set -euo pipefail
if [ -f tests/test_runtime.py ]; then
python tests/test_runtime.py
else
echo "No runtime smoke test file found"
exit 1
fi

- name: Install integration test dependencies
run: pip install pytest pymongo pytest-asyncio

- name: Run integration tests
env:
MONGODB_URI: mongodb://admin:mongodb@localhost:27017/
run: pytest tests/test_integration.py -v
2 changes: 1 addition & 1 deletion examples/why/requirements.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
fastapi
motor[srv]
pymongo[srv]
uvicorn
jinja2
beanie
10 changes: 4 additions & 6 deletions examples/why/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,17 @@ lazy-model==0.2.0
# via beanie
markupsafe==2.1.5
# via jinja2
motor[srv]==3.4.0
# via
# -r requirements.in
# beanie
pydantic==2.7.1
# via
# beanie
# fastapi
# lazy-model
pydantic-core==2.18.2
# via pydantic
pymongo[srv]==4.6.3
# via motor
pymongo[srv]==4.13.1
# via
# -r requirements.in
# beanie
sniffio==1.3.1
# via anyio
starlette==0.37.2
Expand Down
6 changes: 3 additions & 3 deletions examples/why/why/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from fastapi import FastAPI

from motor.motor_asyncio import AsyncIOMotorClient
from pymongo import AsyncMongoClient
# from docbridge import Document, Field, SequenceField
from beanie import Document, init_beanie
from pydantic import BaseModel, Field
Expand All @@ -16,8 +16,8 @@
@asynccontextmanager
async def db_lifespan(app: FastAPI):
# Startup
app.mongodb_client = motor = AsyncIOMotorClient(CONNECTION_STRING)
app.database = db = motor.get_database("why")
app.mongodb_client = mongo_client = AsyncMongoClient(CONNECTION_STRING)
app.database = db = mongo_client.get_database("why")
ping_response = await db.command("ping")
if int(ping_response["ok"]) != 1:
raise Exception("Problem connecting to database cluster.")
Expand Down
Empty file added tests/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import pytest_asyncio
from motor.motor_asyncio import AsyncIOMotorClient as MotorClient
from pymongo import AsyncMongoClient as MotorClient


@pytest_asyncio.fixture(scope="session")
Expand Down
175 changes: 175 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"""Integration tests for docbridge.

Tests real MongoDB CRUD operations for the profiles collection used by the
docbridge FastAPI + Beanie example.

Requires a running MongoDB instance. Set MONGODB_URI (default:
mongodb://admin:mongodb@localhost:27017/) or the tests will be skipped.
"""

import os
import asyncio
import pytest
from datetime import datetime
from pymongo import MongoClient
from bson import ObjectId

MONGODB_URI = os.environ.get("MONGODB_URI", "mongodb://admin:mongodb@localhost:27017/")
TEST_DB = "docbridge_integration_test"


@pytest.fixture(scope="module")
def db():
client = MongoClient(MONGODB_URI, serverSelectionTimeoutMS=2000)
try:
client.admin.command("ping")
except Exception:
client.close()
pytest.skip(f"MongoDB not reachable at {MONGODB_URI}")
database = client[TEST_DB]
yield database
client.drop_database(TEST_DB)
client.close()


def test_mongodb_ping():
client = MongoClient(MONGODB_URI, serverSelectionTimeoutMS=2000)
try:
result = client.admin.command("ping")
assert result.get("ok") == 1.0
except Exception:
pytest.skip(f"MongoDB not reachable at {MONGODB_URI}")
finally:
client.close()


def test_profile_crud(db):
"""profiles collection: insert, find, update, delete a profile."""
profiles = db["profiles"]

profile_id = ObjectId()
profile = {
"_id": profile_id,
"user_id": "u_001",
"user_name": "jdoe",
"full_name": "Jane Doe",
"birth_date": datetime(1990, 5, 15),
"email": "jane.doe@example.com",
"followers": [{"user_id": "u_002"}, {"user_id": "u_003"}],
}

# Create
result = profiles.insert_one(profile)
assert result.inserted_id == profile_id

# Read by user_id
found = profiles.find_one({"user_id": "u_001"})
assert found["user_name"] == "jdoe"
assert found["full_name"] == "Jane Doe"
assert len(found["followers"]) == 2

# Update
profiles.update_one(
{"_id": profile_id},
{"$push": {"followers": {"user_id": "u_004"}}}
)
updated = profiles.find_one({"_id": profile_id})
assert len(updated["followers"]) == 3

# Delete
delete_result = profiles.delete_one({"_id": profile_id})
assert delete_result.deleted_count == 1
assert profiles.find_one({"_id": profile_id}) is None


def test_profile_not_found(db):
"""profiles collection: querying a non-existent user_id returns None."""
profiles = db["profiles"]
assert profiles.find_one({"user_id": "nonexistent_user"}) is None


def test_profile_follower_query(db):
"""profiles collection: query profiles that have a specific follower."""
profiles = db["profiles"]

ids = [ObjectId(), ObjectId()]
docs = [
{
"_id": ids[0],
"user_id": "u_010",
"user_name": "alice",
"full_name": "Alice Smith",
"birth_date": datetime(1992, 3, 10),
"email": "alice@example.com",
"followers": [{"user_id": "u_999"}],
},
{
"_id": ids[1],
"user_id": "u_011",
"user_name": "bob",
"full_name": "Bob Jones",
"birth_date": datetime(1988, 7, 20),
"email": "bob@example.com",
"followers": [],
},
]
profiles.insert_many(docs)

followed_by_999 = list(
profiles.find({"followers.user_id": "u_999", "_id": {"$in": ids}})
)
assert len(followed_by_999) == 1
assert followed_by_999[0]["user_name"] == "alice"

# Cleanup
profiles.delete_many({"_id": {"$in": ids}})


def test_read_item_via_beanie(db):
"""Run the read_item endpoint logic against real MongoDB via Beanie."""
try:
from beanie import init_beanie, Document
from pymongo import AsyncMongoClient
from pydantic import BaseModel, Field

class Follower(BaseModel):
user_id: str

class ProfileDoc(Document):
user_id: str
user_name: str
full_name: str
birth_date: datetime
email: str
followers: list

class Settings:
name = "profiles"

async def _run():
client = AsyncMongoClient(MONGODB_URI, serverSelectionTimeoutMS=3000)
database = client[TEST_DB]
await init_beanie(database=database, document_models=[ProfileDoc])

# Insert via Beanie
p = ProfileDoc(
user_id="u_beanie_test",
user_name="beanie_user",
full_name="Beanie Tester",
birth_date=datetime(1995, 1, 1),
email="beanie@test.com",
followers=[],
)
await p.insert()

# Query via Beanie (mimics read_item endpoint)
found = await ProfileDoc.find_one({"user_id": "u_beanie_test"})
assert found is not None
assert found.user_name == "beanie_user"

await found.delete()
client.close()

asyncio.run(_run())
except ImportError:
pytest.skip("beanie not installed")
Loading
Loading