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
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
generate:
docker-compose run --rm -T protoc
docker-compose run --rm -T protoc

test:
./tools/tests/run_tests.sh
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
version: '3.8'

services:
mongodb-test:
image: mongo:latest
container_name: test_mongodb
ports:
- "27017:27017"
environment:
MONGO_INITDB_DATABASE: test_stock_db
protoc:
build:
context: ./tools/protoc
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies = [
"markupsafe==3.0.2",
"polygon-api-client==1.14.5",
"protobuf==5.29.4",
"pymongo>=4.13.0",
"python-dateutil==2.9.0.post0",
"python-dotenv==1.1.0",
"pyyaml==6.0.2",
Expand All @@ -25,3 +26,8 @@ dependencies = [
"validate-email==1.3",
"websockets==14.2",
]

[dependency-groups]
dev = [
"pytest>=8.3.5",
]
5 changes: 5 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[pytest]
python_files = test_*.py
python_functions = test_*
addopts = -v --tb=short
testpaths = src/tests
9 changes: 6 additions & 3 deletions src/adapters/repositories/stock/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from abc import ABC, abstractmethod
from domain.stock import CreateStock

class AbstractCustomerRepository(ABC):

class AbstractStockRepository(ABC):
@abstractmethod
def create(self):
...
def create(self, stock: CreateStock) -> str:
"""Create a new stock entry in the repository."""
...
28 changes: 28 additions & 0 deletions src/adapters/repositories/stock/stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from pymongo import MongoClient
from pymongo.database import Database
from .base import AbstractStockRepository
from domain.stock import CreateStock


class StockRepository(AbstractStockRepository):
def __init__(self, mongo_client: MongoClient, database_name: str = "stock_db"):
self.client = mongo_client
self.db: Database = self.client[database_name]
self.collection = self.db["stocks"]

def create(self, stock: CreateStock) -> str:
stock_dict = {
"user_id": stock.user_id,
"symbol": stock.symbol,
"price": stock.price,
"quantity": stock.quantity,
"action_type": stock.action_type.value,
"created_at": stock.created_at,
"updated_at": stock.created_at,
}

result = self.collection.insert_one(stock_dict)
return str(result.inserted_id)

def __del__(self):
self.client.close()
3 changes: 2 additions & 1 deletion src/domain/stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
from datetime import datetime
from enum import Enum


class ActionType(Enum):
BUY = "BUY"
SELL = "SELL"
TRANSFER = "TRANSFER"


@dataclass
class CreateStock:
user_id: int
Expand All @@ -15,4 +17,3 @@ class CreateStock:
quantity: int
action_type: ActionType
created_at: datetime

23 changes: 13 additions & 10 deletions src/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@

load_dotenv()

class StockService(stock_pb2_grpc.StockService):
def Create(self, request, context):
return stock_pb2.CreateResp(id='123')

class StockService(stock_pb2_grpc.StockService):
def Create(self, request, context):
return stock_pb2.CreateResp(id="123")


def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
stock_pb2_grpc.add_StockServiceServicer_to_server(StockService(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
stock_pb2_grpc.add_StockServiceServicer_to_server(StockService(), server)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
serve()
if __name__ == "__main__":
serve()
142 changes: 142 additions & 0 deletions src/tests/adapters/repositories/test_stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import pytest
from datetime import datetime
from pymongo import MongoClient
from bson.objectid import ObjectId
from domain.stock import CreateStock, ActionType
from adapters.repositories.stock.stock import StockRepository


@pytest.fixture(scope="module")
def mongo_client():
client = MongoClient("mongodb://localhost:27017")
yield client
client.drop_database("test_stock_db")
client.close()


@pytest.fixture(scope="module")
def stock_repository(mongo_client):
return StockRepository(mongo_client, database_name="test_stock_db")


@pytest.fixture(scope="function", autouse=True)
def clear_collection(stock_repository):
stock_repository.collection.delete_many({})
yield


@pytest.fixture
def sample_stock():
return CreateStock(
user_id=1,
symbol="AAPL",
price=150.25,
quantity=100,
action_type=ActionType.BUY,
created_at=datetime.now(datetime.timezone.utc),
)


class TestStockRepository:
def test_create_stock(self, stock_repository):
mock_stock = CreateStock(
user_id=1,
symbol="AAPL",
price=150.25,
quantity=100,
action_type=ActionType.BUY,
created_at=datetime.utcnow(),
)

expected_result = {
"user_id": mock_stock.user_id,
"symbol": mock_stock.symbol,
"price": mock_stock.price,
"quantity": mock_stock.quantity,
"action_type": mock_stock.action_type.value,
"created_at": mock_stock.created_at,
"updated_at": mock_stock.created_at,
}

stock_id = stock_repository.create(mock_stock)

# Assertion
stock_data = stock_repository.collection.find_one({"_id": ObjectId(stock_id)})
fields_to_exclude = ["_id", "created_at", "updated_at"]
stock_data_filtered = {
k: v for k, v in stock_data.items() if k not in fields_to_exclude
}
expected_result_filtered = {
k: v for k, v in expected_result.items() if k not in fields_to_exclude
}

assert stock_data_filtered == expected_result_filtered

def test_create_multiple_stocks(self, stock_repository):
# Create mock stocks
mock_stock1 = CreateStock(
user_id=1,
symbol="TSLA",
price=110.25,
quantity=50,
action_type=ActionType.BUY,
created_at=datetime.utcnow(),
)
mock_stock2 = CreateStock(
user_id=1,
symbol="GOOGL",
price=2500.50,
quantity=10,
action_type=ActionType.SELL,
created_at=datetime.utcnow(),
)

# Define expected results
expected_result1 = {
"user_id": mock_stock1.user_id,
"symbol": mock_stock1.symbol,
"price": mock_stock1.price,
"quantity": mock_stock1.quantity,
"action_type": mock_stock1.action_type.value,
"created_at": mock_stock1.created_at,
"updated_at": mock_stock1.created_at,
}
expected_result2 = {
"user_id": mock_stock2.user_id,
"symbol": mock_stock2.symbol,
"price": mock_stock2.price,
"quantity": mock_stock2.quantity,
"action_type": mock_stock2.action_type.value,
"created_at": mock_stock2.created_at,
"updated_at": mock_stock2.created_at,
}
expected_data = [expected_result1, expected_result2]

# Create stocks in the repository
_ = stock_repository.create(mock_stock1)
_ = stock_repository.create(mock_stock2)

# Retrieve all stocks from MongoDB
actual_data = list(stock_repository.collection.find({}))

# Fields to exclude
fields_to_exclude = {"_id", "created_at", "updated_at"}

# Loop through each dictionary and exclude unwanted fields (a list of dictionaries)
normalized_expected = [
{k: v for k, v in item.items() if k not in fields_to_exclude}
for item in expected_data
]
normalized_actual = [
{k: v for k, v in item.items() if k not in fields_to_exclude}
for item in actual_data
]

# Convert each dictionary to a tuple of sorted items and create sets
expected_set = {tuple(item.items()) for item in normalized_expected}
actual_set = {tuple(item.items()) for item in normalized_actual}

# Compare the sets
assert (
expected_set == actual_set
), f"Expected {expected_set}, but got {actual_set}"
26 changes: 26 additions & 0 deletions src/tests/tools/wait_for_mongo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import time
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError


def wait_for_mongo(host="localhost", port=27017, timeout=30, db_name="test_stock_db"):
"""Wait for MongoDB to be ready by attempting to connect and ping."""
start_time = time.time()
while time.time() - start_time < timeout:
try:
client = MongoClient(
f"mongodb://{host}:{port}", serverSelectionTimeoutMS=1000
)
client.admin.command("ping") # Ping the server to check if it's ready
client.drop_database(db_name) # Ensure clean state
client.close()
print("MongoDB is ready and test database cleared!")
return True
except (ConnectionFailure, ServerSelectionTimeoutError):
print("MongoDB not ready, retrying...")
time.sleep(1)
raise TimeoutError("MongoDB did not become ready within the timeout period")


if __name__ == "__main__":
wait_for_mongo()
48 changes: 48 additions & 0 deletions tools/tests/run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash

# Define color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m'

# Function to print a line of repeated emojis
print_emoji_line() {
local emoji=$1
local color=$2
local cols=$(tput cols) # Get terminal width
local line=""

# Calculate how many emojis fit in the terminal width (approximate, as emojis vary in width)
for ((i=0; i<cols/2; i++)); do
line="${line}${emoji}"
done
echo -e "${color}${line}"
}

# Function to clean up resources
cleanup() {
echo -e "${BLUE}"
print_emoji_line "<=" "${BLUE}"
echo -e "<= Cleaning up resources..."
print_emoji_line "<=" "${BLUE}"
docker-compose down -v mongodb-test
rm -rf __pycache__ tests/__pycache__
}

# Set trap to call cleanup on exit (success or failure)
trap cleanup EXIT

echo -e "${YELLOW}"
print_emoji_line "=>" "${YELLOW}"
echo -e "=> Starting test environment..."
print_emoji_line "=>" "${YELLOW}"
docker-compose up -d mongodb-test
uv run src/tests/tools/wait_for_mongo.py
PYTHONPATH=./src python -m pytest src/tests/
if [ $? -eq 0 ]; then
echo -e "${GREEN}🎉 Tests passed successfully!"
else
echo -e "${RED}❌ Tests failed!"
fi
Loading