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
125 changes: 125 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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: Install runtime test dependencies
run: pip install Pillow fastapi pymongo pydantic uvicorn

- 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

- 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 backend/requirements.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
fastapi[all]
motor[srv]
pymongo[srv]
boto3
#aiobotocore
pillow
6 changes: 2 additions & 4 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ jmespath==1.0.1
# botocore
markupsafe==2.1.5
# via jinja2
motor==3.4.0
# via -r requirements.ini
orjson==3.10.2
# via fastapi
pillow==10.3.0
Expand All @@ -73,8 +71,8 @@ pydantic-extra-types==2.7.0
# via fastapi
pydantic-settings==2.2.1
# via fastapi
pymongo==4.7.1
# via motor
pymongo==4.13.1
# via -r requirements.ini
python-dateutil==2.9.0.post0
# via botocore
python-dotenv==1.0.1
Expand Down
Empty file added tests/__init__.py
Empty file.
172 changes: 172 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""Integration tests for celeb-matcher-farm.

Tests real MongoDB CRUD operations for the attendee/celeb_images collection
used by the FastAPI + AWS Bedrock celebrity matching backend.

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

import os
import base64
import pytest
from pymongo import MongoClient
from bson import ObjectId

MONGODB_URI = os.environ.get("MONGODB_URI", "mongodb://admin:mongodb@localhost:27017/")
TEST_DB = "celeb_matcher_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_attendee_record_crud(db):
"""celeb_images collection: insert, find, update, delete an attendee."""
celeb_images = db["celeb_images"]

# Create a fake embedding (1024 floats simulating Bedrock output)
embedding = [0.01 * i for i in range(1024)]

doc_id = ObjectId()
record = {
"_id": doc_id,
"name": "Test Attendee",
"email": "attendee@example.com",
"embedding": embedding,
"image_b64": base64.b64encode(b"fake-image-bytes").decode(),
}

# Create
result = celeb_images.insert_one(record)
assert result.inserted_id == doc_id

# Read
found = celeb_images.find_one({"_id": doc_id})
assert found["name"] == "Test Attendee"
assert len(found["embedding"]) == 1024

# Update
celeb_images.update_one({"_id": doc_id}, {"$set": {"name": "Updated Attendee"}})
updated = celeb_images.find_one({"_id": doc_id})
assert updated["name"] == "Updated Attendee"

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


def test_query_attendees_by_email(db):
"""celeb_images collection: find by email field."""
celeb_images = db["celeb_images"]

ids = [ObjectId(), ObjectId()]
docs = [
{
"_id": ids[0],
"name": "Alice",
"email": "alice@conference.example",
"embedding": [0.1] * 1024,
},
{
"_id": ids[1],
"name": "Bob",
"email": "bob@conference.example",
"embedding": [0.2] * 1024,
},
]
celeb_images.insert_many(docs)

found = celeb_images.find_one({"email": "alice@conference.example", "_id": {"$in": ids}})
assert found is not None
assert found["name"] == "Alice"

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


def test_vector_search_index_structure(db):
"""celeb_images: verify documents have the embedding field needed for vector search."""
celeb_images = db["celeb_images"]

doc_id = ObjectId()
celeb_images.insert_one({
"_id": doc_id,
"name": "Carol",
"embedding": [float(i) / 1024 for i in range(1024)],
})

found = celeb_images.find_one({"_id": doc_id})
assert "embedding" in found
assert isinstance(found["embedding"], list)
assert len(found["embedding"]) == 1024
# All values should be valid floats
assert all(isinstance(v, float) for v in found["embedding"])

celeb_images.delete_one({"_id": doc_id})


def test_standardize_image_function():
"""standardize_image() produces a valid base64-encoded JPEG string."""
try:
import sys
from pathlib import Path
import importlib.util
import types

backend_src = Path(__file__).resolve().parents[1] / "backend" / "src"

# Stub boto3 and AWS dependencies
for mod_name in ("boto3", "uvicorn"):
if mod_name not in sys.modules:
stub = types.ModuleType(mod_name)
sys.modules[mod_name] = stub

os.environ.setdefault("MONGODB_URI", MONGODB_URI)
os.environ.setdefault("AWS_ACCESS_KEY", "test")
os.environ.setdefault("AWS_SECRET_KEY", "test")

spec = importlib.util.spec_from_file_location(
"celeb_server_int", backend_src / "server.py"
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)

# Create a minimal 2x2 JPEG image
from PIL import Image
import io
img = Image.new("RGB", (2, 2), color=(255, 0, 0))
buf = io.BytesIO()
img.save(buf, format="JPEG")
buf.seek(0)
input_b64 = base64.b64encode(buf.getvalue()).decode()

result_b64 = mod.standardize_image(input_b64)
decoded = base64.b64decode(result_b64)
out_img = Image.open(io.BytesIO(decoded))
assert out_img.size == (512, 512)
assert out_img.mode == "RGB"
except Exception as exc:
pytest.skip(f"standardize_image test skipped: {exc}")
46 changes: 46 additions & 0 deletions tests/test_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import base64
import importlib.util
import io
import os
import sys
import types
import unittest
from pathlib import Path

from PIL import Image


class RuntimeTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
os.environ.setdefault("MONGODB_URI", "mongodb://example/test")
os.environ.setdefault("AWS_ACCESS_KEY", "x")
os.environ.setdefault("AWS_SECRET_KEY", "y")

if "boto3" not in sys.modules:
boto3 = types.ModuleType("boto3")
boto3.client = lambda *args, **kwargs: object()
sys.modules["boto3"] = boto3

target = Path(__file__).resolve().parents[1] / "backend" / "src" / "server.py"
spec = importlib.util.spec_from_file_location("celeb_server", target)
cls.mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cls.mod)

def test_standardize_image_and_payload(self):
img = Image.new("RGB", (2, 2), color=(255, 0, 0))
buff = io.BytesIO()
img.save(buff, format="JPEG")
b64 = base64.b64encode(buff.getvalue()).decode("utf-8")

out = self.mod.standardize_image(b64)
self.assertIsInstance(out, str)
decoded = base64.b64decode(out)
self.assertGreater(len(decoded), 0)

payload = self.mod.SearchPayload(img="data:image/jpeg;base64," + b64, compareWithOtherAttendees=False)
self.assertFalse(payload.compareWithOtherAttendees)


if __name__ == "__main__":
unittest.main()
Loading