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
31 changes: 31 additions & 0 deletions src/handler/stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ def Create(self, request, context):
context.set_details("Internal server error")
raise grpc.RpcError("Internal server error")

def List(self, request, context):
try:
user_id = request.user_id
stock_list = self.stock_usecase.list(user_id)

return stock_pb2.ListResp(stock_list=self._convert_to_proto_stock_list(stock_list))
except Exception as e:
logging.error(
"Failed to list stocks for user_id=%s: %s",
request.user_id,
str(e),
)
context.set_code(grpc.StatusCode.INTERNAL)
context.set_details("Internal server error")
raise grpc.RpcError("Internal server error")

def _map_action_type(self, action: int) -> ActionType:
if action not in ACTION_MAP:
raise ValueError(f"Invalid action type: {action}. Must be 1 (BUY), 2 (SELL), or 3 (TRANSFER).")
Expand All @@ -57,3 +73,18 @@ def _validate_create_request(self, request):
raise ValueError("price must be greater than 0")
if request.quantity <= 0:
raise ValueError("quantity must be greater than 0")

def _convert_to_proto_stock_list(self, stock_list):
return [
stock_pb2.Stock(
id=stock.id,
user_id=stock.user_id,
symbol=stock.symbol,
price=stock.price,
quantity=stock.quantity,
action=stock.action_type.value,
created_at=stock.created_at,
updated_at=stock.updated_at,
)
for stock in stock_list
]
21 changes: 21 additions & 0 deletions src/proto/stock.proto
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ message Action {
}
}

message Stock {
string id = 1 [json_name = "id"];
int32 user_id = 2 [json_name = "user_id"];
string symbol = 3 [json_name = "symbol"];
double price = 4 [json_name = "price"];
int32 quantity = 5 [json_name = "quantity"];
string action = 6 [json_name = "action"];
google.protobuf.Timestamp created_at = 7 [json_name = "created_at"];
google.protobuf.Timestamp updated_at = 8 [json_name = "updated_at"];
}

message CreateReq {
int32 user_id = 1 [json_name = "user_id"];
string symbol = 2 [json_name = "symbol"];
Expand All @@ -27,6 +38,16 @@ message CreateResp {
string id = 1 [json_name = "id"];
}

message ListReq {
int32 user_id = 1 [json_name = "user_id"];
}

message ListResp {
repeated Stock stock_list = 1 [json_name = "stock_list"];
}

service StockService {
rpc Create (CreateReq) returns (CreateResp) {}

rpc List (ListReq) returns (ListResp) {}
}
20 changes: 13 additions & 7 deletions src/proto/stock_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions src/proto/stock_pb2_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def __init__(self, channel):
request_serializer=proto_dot_stock__pb2.CreateReq.SerializeToString,
response_deserializer=proto_dot_stock__pb2.CreateResp.FromString,
_registered_method=True)
self.List = channel.unary_unary(
'/stock.StockService/List',
request_serializer=proto_dot_stock__pb2.ListReq.SerializeToString,
response_deserializer=proto_dot_stock__pb2.ListResp.FromString,
_registered_method=True)


class StockServiceServicer(object):
Expand All @@ -50,6 +55,12 @@ def Create(self, request, context):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def List(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')


def add_StockServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
Expand All @@ -58,6 +69,11 @@ def add_StockServiceServicer_to_server(servicer, server):
request_deserializer=proto_dot_stock__pb2.CreateReq.FromString,
response_serializer=proto_dot_stock__pb2.CreateResp.SerializeToString,
),
'List': grpc.unary_unary_rpc_method_handler(
servicer.List,
request_deserializer=proto_dot_stock__pb2.ListReq.FromString,
response_serializer=proto_dot_stock__pb2.ListResp.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'stock.StockService', rpc_method_handlers)
Expand Down Expand Up @@ -95,3 +111,30 @@ def Create(request,
timeout,
metadata,
_registered_method=True)

@staticmethod
def List(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/stock.StockService/List',
proto_dot_stock__pb2.ListReq.SerializeToString,
proto_dot_stock__pb2.ListResp.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
88 changes: 86 additions & 2 deletions src/tests/test_stock_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import grpc
import proto.stock_pb2 as stock_pb2

from datetime import datetime
from datetime import datetime, timezone
from unittest.mock import Mock

from domain.stock import CreateStock, ActionType
from domain.stock import CreateStock, ActionType, Stock
from handler.stock import StockService
from usecase.base import AbstractStockUsecase

Expand Down Expand Up @@ -165,3 +165,87 @@ def test_internal_error(self, mock_stock_usecase, mock_context, valid_request):
mock_context.set_code.assert_called_once_with(grpc.StatusCode.INTERNAL)
mock_context.set_details.assert_called_once_with("Internal server error")
mock_stock_usecase.create.assert_called_once()


class TestStockServiceList:
# Fixture to create a mock stock_usecase
@pytest.fixture
def mock_stock_usecase(self):
usecase = Mock(spec=AbstractStockUsecase)
usecase.list.return_value = [
Stock(
id="stock_123",
user_id=1,
symbol="AAPL",
price=100.0,
quantity=10,
action_type=ActionType.BUY,
created_at=datetime(2023, 1, 1, tzinfo=timezone.utc),
updated_at=datetime(2023, 1, 1, tzinfo=timezone.utc), # Include if required
),
Stock(
id="stock_124",
user_id=1,
symbol="GOOGL",
price=1500.0,
quantity=5,
action_type=ActionType.SELL,
created_at=datetime(2023, 1, 2, tzinfo=timezone.utc),
updated_at=datetime(2023, 1, 2, tzinfo=timezone.utc), # Include if required
),
]
return usecase

# Fixture to create a mock gRPC context
@pytest.fixture
def mock_context(self):
context = Mock()
context.set_code = Mock()
context.set_details = Mock()
return context

# Fixture to create a valid gRPC request
@pytest.fixture
def valid_request(self):
request = Mock()
request.user_id = 1
return request

def test_success(self, mock_stock_usecase, mock_context, valid_request):
# Arrange
service = StockService(mock_stock_usecase)

# Action
response = service.List(valid_request, mock_context)

# Assertion
assert isinstance(response, stock_pb2.ListResp)
assert len(response.stock_list) == 2
assert response.stock_list[0].id == "stock_123"
assert response.stock_list[0].user_id == 1
assert response.stock_list[0].symbol == "AAPL"
assert response.stock_list[0].price == 100.0
assert response.stock_list[0].quantity == 10
assert response.stock_list[0].action == ActionType.BUY.value
assert response.stock_list[1].id == "stock_124"
assert response.stock_list[1].user_id == 1
assert response.stock_list[1].symbol == "GOOGL"
assert response.stock_list[1].price == 1500.0
assert response.stock_list[1].quantity == 5
assert response.stock_list[1].action == ActionType.SELL.value
mock_stock_usecase.list.assert_called_once_with(1)
mock_context.set_code.assert_not_called()
mock_context.set_details.assert_not_called()

def test_internal_error(self, mock_stock_usecase, mock_context, valid_request):
# Arrange
service = StockService(mock_stock_usecase)
mock_stock_usecase.list.side_effect = Exception("Database error") # Simulate internal error

# Act/Assertion
with pytest.raises(grpc.RpcError) as exc_info:
service.List(valid_request, mock_context)
assert str(exc_info.value) == "Internal server error"
mock_context.set_code.assert_called_once_with(grpc.StatusCode.INTERNAL)
mock_context.set_details.assert_called_once_with("Internal server error")
mock_stock_usecase.list.assert_called_once_with(1)