From d63ae80ff7ce72642affa975390c0ad47b670ded Mon Sep 17 00:00:00 2001 From: Achille Roussel Date: Wed, 24 Apr 2024 18:10:29 -0700 Subject: [PATCH 1/3] test: drop FastAPI dependency Signed-off-by: Achille Roussel --- src/dispatch/test/client.py | 13 +--- tests/test_fastapi.py | 120 +++++++++++++++++++++++++++++++++--- tests/test_full.py | 99 ----------------------------- 3 files changed, 113 insertions(+), 119 deletions(-) delete mode 100644 tests/test_full.py diff --git a/src/dispatch/test/client.py b/src/dispatch/test/client.py index 01078aec..04d2fa9e 100644 --- a/src/dispatch/test/client.py +++ b/src/dispatch/test/client.py @@ -1,10 +1,8 @@ from datetime import datetime from typing import Optional -import fastapi import grpc import httpx -from fastapi.testclient import TestClient from dispatch.sdk.v1 import function_pb2 as function_pb from dispatch.sdk.v1 import function_pb2_grpc as function_grpc @@ -22,7 +20,7 @@ class EndpointClient: Note that this is different from dispatch.Client, which is a client for the Dispatch API. The EndpointClient is a client similar to the one that Dispatch itself would use to interact with an endpoint that provides - functions, for example a FastAPI app. + functions. """ def __init__( @@ -54,15 +52,6 @@ def from_url(cls, url: str, signing_key: Optional[Ed25519PrivateKey] = None): http_client = httpx.Client(base_url=url) return EndpointClient(http_client, signing_key) - @classmethod - def from_app( - cls, app: fastapi.FastAPI, signing_key: Optional[Ed25519PrivateKey] = None - ): - """Returns an EndpointClient for a Dispatch endpoint bound to a - FastAPI app instance.""" - http_client = TestClient(app) - return EndpointClient(http_client, signing_key) - class _HttpxGrpcChannel(grpc.Channel): def __init__( diff --git a/tests/test_fastapi.py b/tests/test_fastapi.py index b3199b6d..32154f66 100644 --- a/tests/test_fastapi.py +++ b/tests/test_fastapi.py @@ -3,28 +3,36 @@ import pickle import struct import unittest -from typing import Any +from typing import Any, Optional from unittest import mock import fastapi import google.protobuf.any_pb2 import google.protobuf.wrappers_pb2 import httpx -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, +) from fastapi.testclient import TestClient +import dispatch from dispatch.experimental.durable.registry import clear_functions from dispatch.fastapi import Dispatch from dispatch.function import Arguments, Error, Function, Input, Output from dispatch.proto import _any_unpickle as any_unpickle from dispatch.sdk.v1 import call_pb2 as call_pb from dispatch.sdk.v1 import function_pb2 as function_pb -from dispatch.signature import parse_verification_key, public_key_from_pem +from dispatch.signature import ( + parse_verification_key, + private_key_from_pem, + public_key_from_pem, +) from dispatch.status import Status -from dispatch.test import EndpointClient +from dispatch.test import DispatchServer, DispatchService, EndpointClient -def create_dispatch_instance(app, endpoint): +def create_dispatch_instance(app: fastapi.FastAPI, endpoint: str): return Dispatch( app, endpoint=endpoint, @@ -33,6 +41,13 @@ def create_dispatch_instance(app, endpoint): ) +def create_endpoint_client( + app: fastapi.FastAPI, signing_key: Optional[Ed25519PrivateKey] = None +): + http_client = TestClient(app) + return EndpointClient(http_client, signing_key) + + class TestFastAPI(unittest.TestCase): def test_Dispatch(self): app = fastapi.FastAPI() @@ -79,8 +94,7 @@ def my_function(input: Input) -> Output: f"You told me: '{input.input}' ({len(input.input)} characters)" ) - client = EndpointClient.from_app(app) - + client = create_endpoint_client(app) pickled = pickle.dumps("Hello World!") input_any = google.protobuf.any_pb2.Any() input_any.Pack(google.protobuf.wrappers_pb2.BytesValue(value=pickled)) @@ -102,6 +116,96 @@ def my_function(input: Input) -> Output: self.assertEqual(output, "You told me: 'Hello World!' (12 characters)") +signing_key = private_key_from_pem( + """ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF +-----END PRIVATE KEY----- +""" +) + +verification_key = public_key_from_pem( + """ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs= +-----END PUBLIC KEY----- +""" +) + + +class TestFullFastapi(unittest.TestCase): + def setUp(self): + self.endpoint_app = fastapi.FastAPI() + endpoint_client = create_endpoint_client(self.endpoint_app, signing_key) + + api_key = "0000000000000000" + self.dispatch_service = DispatchService( + endpoint_client, api_key, collect_roundtrips=True + ) + self.dispatch_server = DispatchServer(self.dispatch_service) + self.dispatch_client = dispatch.Client( + api_key, api_url=self.dispatch_server.url + ) + + self.dispatch = Dispatch( + self.endpoint_app, + endpoint="http://function-service", # unused + verification_key=verification_key, + api_key=api_key, + api_url=self.dispatch_server.url, + ) + + self.dispatch_server.start() + + def tearDown(self): + self.dispatch_server.stop() + + def test_simple_end_to_end(self): + # The FastAPI server. + @self.dispatch.function + def my_function(name: str) -> str: + return f"Hello world: {name}" + + call = my_function.build_call(52) + self.assertEqual(call.function.split(".")[-1], "my_function") + + # The client. + [dispatch_id] = self.dispatch_client.dispatch([my_function.build_call(52)]) + + # Simulate execution for testing purposes. + self.dispatch_service.dispatch_calls() + + # Validate results. + roundtrips = self.dispatch_service.roundtrips[dispatch_id] + self.assertEqual(len(roundtrips), 1) + _, response = roundtrips[0] + self.assertEqual(any_unpickle(response.exit.result.output), "Hello world: 52") + + def test_simple_missing_signature(self): + @self.dispatch.function + async def my_function(name: str) -> str: + return f"Hello world: {name}" + + call = my_function.build_call(52) + self.assertEqual(call.function.split(".")[-1], "my_function") + + [dispatch_id] = self.dispatch_client.dispatch([call]) + + self.dispatch_service.endpoint_client = create_endpoint_client( + self.endpoint_app + ) # no signing key + try: + self.dispatch_service.dispatch_calls() + except httpx.HTTPStatusError as e: + assert e.response.status_code == 403 + assert e.response.json() == { + "code": "permission_denied", + "message": 'Expected "Signature-Input" header field to be present', + } + else: + assert False, "Expected HTTPStatusError" + + def response_output(resp: function_pb.RunResponse) -> Any: return any_unpickle(resp.exit.result.output) @@ -120,7 +224,7 @@ def root(): self.app, endpoint="https://127.0.0.1:9999" ) self.http_client = TestClient(self.app) - self.client = EndpointClient.from_app(self.app) + self.client = create_endpoint_client(self.app) def execute( self, func: Function, input=None, state=None, calls=None diff --git a/tests/test_full.py b/tests/test_full.py deleted file mode 100644 index f9b142b1..00000000 --- a/tests/test_full.py +++ /dev/null @@ -1,99 +0,0 @@ -import unittest - -import fastapi -import httpx - -import dispatch -from dispatch.fastapi import Dispatch -from dispatch.proto import _any_unpickle as any_unpickle -from dispatch.signature import private_key_from_pem, public_key_from_pem -from dispatch.test import DispatchServer, DispatchService, EndpointClient - -signing_key = private_key_from_pem( - """ ------BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF ------END PRIVATE KEY----- -""" -) - -verification_key = public_key_from_pem( - """ ------BEGIN PUBLIC KEY----- -MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs= ------END PUBLIC KEY----- -""" -) - - -class TestFullFastapi(unittest.TestCase): - def setUp(self): - self.endpoint_app = fastapi.FastAPI() - endpoint_client = EndpointClient.from_app(self.endpoint_app, signing_key) - - api_key = "0000000000000000" - self.dispatch_service = DispatchService( - endpoint_client, api_key, collect_roundtrips=True - ) - self.dispatch_server = DispatchServer(self.dispatch_service) - self.dispatch_client = dispatch.Client( - api_key, api_url=self.dispatch_server.url - ) - - self.dispatch = Dispatch( - self.endpoint_app, - endpoint="http://function-service", # unused - verification_key=verification_key, - api_key=api_key, - api_url=self.dispatch_server.url, - ) - - self.dispatch_server.start() - - def tearDown(self): - self.dispatch_server.stop() - - def test_simple_end_to_end(self): - # The FastAPI server. - @self.dispatch.function - def my_function(name: str) -> str: - return f"Hello world: {name}" - - call = my_function.build_call(52) - self.assertEqual(call.function.split(".")[-1], "my_function") - - # The client. - [dispatch_id] = self.dispatch_client.dispatch([my_function.build_call(52)]) - - # Simulate execution for testing purposes. - self.dispatch_service.dispatch_calls() - - # Validate results. - roundtrips = self.dispatch_service.roundtrips[dispatch_id] - self.assertEqual(len(roundtrips), 1) - _, response = roundtrips[0] - self.assertEqual(any_unpickle(response.exit.result.output), "Hello world: 52") - - def test_simple_missing_signature(self): - @self.dispatch.function - async def my_function(name: str) -> str: - return f"Hello world: {name}" - - call = my_function.build_call(52) - self.assertEqual(call.function.split(".")[-1], "my_function") - - [dispatch_id] = self.dispatch_client.dispatch([call]) - - self.dispatch_service.endpoint_client = EndpointClient.from_app( - self.endpoint_app - ) # no signing key - try: - self.dispatch_service.dispatch_calls() - except httpx.HTTPStatusError as e: - assert e.response.status_code == 403 - assert e.response.json() == { - "code": "permission_denied", - "message": 'Expected "Signature-Input" header field to be present', - } - else: - assert False, "Expected HTTPStatusError" From 57a55c8ffcad4cdc7703d74b354fb1b698874066 Mon Sep 17 00:00:00 2001 From: Achille Roussel Date: Wed, 24 Apr 2024 18:19:52 -0700 Subject: [PATCH 2/3] fix examples Signed-off-by: Achille Roussel --- examples/auto_retry/test_app.py | 2 +- examples/getting_started/test_app.py | 2 +- examples/github_stats/test_app.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/auto_retry/test_app.py b/examples/auto_retry/test_app.py index 81555369..ba3a440e 100644 --- a/examples/auto_retry/test_app.py +++ b/examples/auto_retry/test_app.py @@ -25,7 +25,7 @@ def test_app(self): from .app import app, dispatch # Setup a fake Dispatch server. - endpoint_client = EndpointClient.from_app(app) + endpoint_client = EndpointClient(TestClient(app)) dispatch_service = DispatchService(endpoint_client, collect_roundtrips=True) with DispatchServer(dispatch_service) as dispatch_server: # Use it when dispatching function calls. diff --git a/examples/getting_started/test_app.py b/examples/getting_started/test_app.py index 3f420e73..39e04efa 100644 --- a/examples/getting_started/test_app.py +++ b/examples/getting_started/test_app.py @@ -24,7 +24,7 @@ def test_app(self): from .app import app, dispatch # Setup a fake Dispatch server. - endpoint_client = EndpointClient.from_app(app) + endpoint_client = EndpointClient(TestClient(app)) dispatch_service = DispatchService(endpoint_client, collect_roundtrips=True) with DispatchServer(dispatch_service) as dispatch_server: # Use it when dispatching function calls. diff --git a/examples/github_stats/test_app.py b/examples/github_stats/test_app.py index dd68ddbe..08b7b24f 100644 --- a/examples/github_stats/test_app.py +++ b/examples/github_stats/test_app.py @@ -24,7 +24,7 @@ def test_app(self): from .app import app, dispatch # Setup a fake Dispatch server. - endpoint_client = EndpointClient.from_app(app) + endpoint_client = EndpointClient(TestClient(app)) dispatch_service = DispatchService(endpoint_client, collect_roundtrips=True) with DispatchServer(dispatch_service) as dispatch_server: # Use it when dispatching function calls. From 831feeac46c49039318b2f346318437dc5bf169f Mon Sep 17 00:00:00 2001 From: Achille Roussel Date: Wed, 24 Apr 2024 19:26:11 -0700 Subject: [PATCH 3/3] remove impossible test validated by mypy type checks Signed-off-by: Achille Roussel --- tests/test_fastapi.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_fastapi.py b/tests/test_fastapi.py index 32154f66..5c2135dc 100644 --- a/tests/test_fastapi.py +++ b/tests/test_fastapi.py @@ -69,10 +69,6 @@ def read_root(): resp = client.post("/dispatch.sdk.v1.FunctionService/Run") self.assertEqual(resp.status_code, 400) - def test_Dispatch_no_app(self): - with self.assertRaises(ValueError): - create_dispatch_instance(None, endpoint="http://127.0.0.1:9999") - @mock.patch.dict(os.environ, {"DISPATCH_ENDPOINT_URL": ""}) def test_Dispatch_no_endpoint(self): app = fastapi.FastAPI()