Skip to content

Commit 069eb3e

Browse files
committed
test: add comprehensive tests for ASGI server support
- Add tests for HTTPServer ASGI/WSGI auto-detection - Add tests for StarletteApplication and UvicornApplication - Add tests for CLI --gateway flag functionality - Add integration tests for async functions with ASGI - Ensure 100% code coverage for new ASGI features - Apply black and isort formatting to test files
1 parent 80385e8 commit 069eb3e

File tree

5 files changed

+423
-2
lines changed

5 files changed

+423
-2
lines changed

tests/test_asgi.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import sys
16+
17+
import flask
18+
import pretend
19+
import pytest
20+
21+
import functions_framework._http
22+
23+
try:
24+
from starlette.applications import Starlette
25+
except ImportError:
26+
pass
27+
28+
29+
def test_httpserver_detects_asgi_app():
30+
flask_app = flask.Flask("test")
31+
flask_wrapper = functions_framework._http.HTTPServer(flask_app, debug=True)
32+
assert flask_wrapper.server_class.__name__ == "FlaskApplication"
33+
34+
starlette_app = Starlette(routes=[])
35+
starlette_wrapper = functions_framework._http.HTTPServer(starlette_app, debug=True)
36+
assert starlette_wrapper.server_class.__name__ == "StarletteApplication"
37+
38+
39+
@pytest.mark.skipif("platform.system() == 'Windows'")
40+
def test_httpserver_production_asgi():
41+
starlette_app = Starlette(routes=[])
42+
wrapper = functions_framework._http.HTTPServer(starlette_app, debug=False)
43+
assert wrapper.server_class.__name__ == "UvicornApplication"
44+
45+
46+
def test_starlette_application_init():
47+
from functions_framework._http.asgi import StarletteApplication
48+
49+
app = pretend.stub()
50+
host = "1.2.3.4"
51+
port = "5678"
52+
53+
# Test debug mode
54+
starlette_app = StarletteApplication(app, host, port, debug=True, custom="value")
55+
assert starlette_app.app == app
56+
assert starlette_app.host == host
57+
assert starlette_app.port == port
58+
assert starlette_app.debug is True
59+
assert starlette_app.options["log_level"] == "debug"
60+
assert starlette_app.options["reload"] is True
61+
assert starlette_app.options["custom"] == "value"
62+
63+
# Test production mode
64+
starlette_app = StarletteApplication(app, host, port, debug=False)
65+
assert starlette_app.options["log_level"] == "error"
66+
assert starlette_app.options["reload"] is False
67+
68+
69+
@pytest.mark.skipif("platform.system() == 'Windows'")
70+
def test_uvicorn_application_init():
71+
from functions_framework._http.gunicorn import UvicornApplication
72+
73+
app = pretend.stub()
74+
host = "1.2.3.4"
75+
port = "1234"
76+
77+
uvicorn_app = UvicornApplication(app, host, port, debug=False)
78+
assert uvicorn_app.app == app
79+
assert uvicorn_app.options["worker_class"] == "uvicorn_worker.UvicornWorker"
80+
assert uvicorn_app.options["bind"] == "1.2.3.4:1234"
81+
assert uvicorn_app.load() == app
82+
83+
84+
def test_httpserver_fallback_on_import_error(monkeypatch):
85+
starlette_app = Starlette(routes=[])
86+
87+
monkeypatch.setitem(sys.modules, "functions_framework._http.gunicorn", None)
88+
89+
wrapper = functions_framework._http.HTTPServer(starlette_app, debug=False)
90+
assert wrapper.server_class.__name__ == "StarletteApplication"
91+
92+
93+
def test_starlette_application_run(monkeypatch):
94+
uvicorn_run_calls = []
95+
96+
def mock_uvicorn_run(app, **kwargs):
97+
uvicorn_run_calls.append((app, kwargs))
98+
99+
uvicorn_stub = pretend.stub(run=mock_uvicorn_run)
100+
monkeypatch.setitem(sys.modules, "uvicorn", uvicorn_stub)
101+
102+
# Clear and re-import to get fresh module with mocked uvicorn
103+
if "functions_framework._http.asgi" in sys.modules:
104+
del sys.modules["functions_framework._http.asgi"]
105+
106+
from functions_framework._http.asgi import StarletteApplication
107+
108+
app = pretend.stub()
109+
host = "1.2.3.4"
110+
port = "5678"
111+
112+
starlette_app = StarletteApplication(app, host, port, debug=True, custom="value")
113+
starlette_app.run()
114+
115+
assert len(uvicorn_run_calls) == 1
116+
assert uvicorn_run_calls[0][0] == app
117+
assert uvicorn_run_calls[0][1] == {
118+
"host": host,
119+
"port": int(port),
120+
"log_level": "debug",
121+
"reload": True,
122+
"custom": "value",
123+
}

tests/test_cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import sys
16+
1517
import pretend
1618
import pytest
1719

@@ -103,3 +105,22 @@ def test_cli(monkeypatch, args, env, create_app_calls, run_calls):
103105
assert result.exit_code == 0
104106
assert create_app.calls == create_app_calls
105107
assert wsgi_server.run.calls == run_calls
108+
109+
110+
def test_asgi_cli(monkeypatch):
111+
asgi_server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
112+
asgi_app = pretend.stub()
113+
114+
create_asgi_app = pretend.call_recorder(lambda *a, **kw: asgi_app)
115+
aio_module = pretend.stub(create_asgi_app=create_asgi_app)
116+
monkeypatch.setitem(sys.modules, "functions_framework.aio", aio_module)
117+
118+
create_server = pretend.call_recorder(lambda *a, **kw: asgi_server)
119+
monkeypatch.setattr(functions_framework._cli, "create_server", create_server)
120+
121+
runner = CliRunner()
122+
result = runner.invoke(_cli, ["--target", "foo", "--gateway", "asgi"])
123+
124+
assert result.exit_code == 0
125+
assert create_asgi_app.calls == [pretend.call("foo", None, "http")]
126+
assert asgi_server.run.calls == [pretend.call("0.0.0.0", 8080)]

tests/test_cli_gateway.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import sys
16+
17+
import pretend
18+
import pytest
19+
20+
from click.testing import CliRunner
21+
22+
from functions_framework._cli import _cli as cli_command
23+
24+
25+
# Test for CLI gateway flag
26+
def test_cli_gateway_default(monkeypatch):
27+
"""Test default gateway is WSGI"""
28+
server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
29+
app = pretend.stub()
30+
31+
create_app = pretend.call_recorder(lambda *a, **kw: app)
32+
create_server = pretend.call_recorder(lambda *a, **kw: server)
33+
34+
from functions_framework import _cli
35+
36+
monkeypatch.setattr(_cli, "create_app", create_app)
37+
monkeypatch.setattr(_cli, "create_server", create_server)
38+
39+
runner = CliRunner()
40+
result = runner.invoke(cli_command, ["--target", "foo"])
41+
42+
assert result.exit_code == 0
43+
assert create_app.calls == [pretend.call("foo", None, "http")]
44+
assert server.run.calls == [pretend.call("0.0.0.0", 8080)]
45+
46+
47+
def test_cli_gateway_wsgi_explicit(monkeypatch):
48+
"""Test explicit --gateway wsgi"""
49+
server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
50+
app = pretend.stub()
51+
52+
create_app = pretend.call_recorder(lambda *a, **kw: app)
53+
create_server = pretend.call_recorder(lambda *a, **kw: server)
54+
55+
from functions_framework import _cli
56+
57+
monkeypatch.setattr(_cli, "create_app", create_app)
58+
monkeypatch.setattr(_cli, "create_server", create_server)
59+
60+
runner = CliRunner()
61+
result = runner.invoke(cli_command, ["--target", "foo", "--gateway", "wsgi"])
62+
63+
assert result.exit_code == 0
64+
assert create_app.calls == [pretend.call("foo", None, "http")]
65+
66+
67+
def test_cli_gateway_asgi(monkeypatch):
68+
"""Test --gateway asgi"""
69+
# Skip this test if aio module is not available
70+
try:
71+
import functions_framework.aio
72+
except ImportError:
73+
pytest.skip("Async support not available")
74+
75+
server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
76+
app = pretend.stub()
77+
78+
# Mock the create_asgi_app function
79+
create_asgi_app = pretend.call_recorder(lambda *a, **kw: app)
80+
monkeypatch.setattr("functions_framework.aio.create_asgi_app", create_asgi_app)
81+
82+
create_server = pretend.call_recorder(lambda *a, **kw: server)
83+
84+
from functions_framework import _cli
85+
86+
monkeypatch.setattr(_cli, "create_server", create_server)
87+
88+
runner = CliRunner()
89+
result = runner.invoke(cli_command, ["--target", "foo", "--gateway", "asgi"])
90+
91+
assert result.exit_code == 0
92+
assert create_asgi_app.calls == [pretend.call("foo", None, "http")]
93+
94+
95+
def test_cli_gateway_asgi_cloudevent(monkeypatch):
96+
"""Test --gateway asgi with cloudevent signature"""
97+
# Skip this test if aio module is not available
98+
try:
99+
import functions_framework.aio
100+
except ImportError:
101+
pytest.skip("Async support not available")
102+
103+
server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
104+
app = pretend.stub()
105+
106+
# Mock the create_asgi_app function
107+
create_asgi_app = pretend.call_recorder(lambda *a, **kw: app)
108+
monkeypatch.setattr("functions_framework.aio.create_asgi_app", create_asgi_app)
109+
110+
create_server = pretend.call_recorder(lambda *a, **kw: server)
111+
112+
from functions_framework import _cli
113+
114+
monkeypatch.setattr(_cli, "create_server", create_server)
115+
116+
runner = CliRunner()
117+
result = runner.invoke(
118+
cli_command,
119+
["--target", "foo", "--gateway", "asgi", "--signature-type", "cloudevent"],
120+
)
121+
122+
assert result.exit_code == 0
123+
assert create_asgi_app.calls == [pretend.call("foo", None, "cloudevent")]
124+
125+
126+
def test_cli_gateway_env_var(monkeypatch):
127+
"""Test GATEWAY environment variable"""
128+
# Skip this test if aio module is not available
129+
try:
130+
import functions_framework.aio
131+
except ImportError:
132+
pytest.skip("Async support not available")
133+
134+
server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
135+
app = pretend.stub()
136+
137+
# Mock the create_asgi_app function
138+
create_asgi_app = pretend.call_recorder(lambda *a, **kw: app)
139+
monkeypatch.setattr("functions_framework.aio.create_asgi_app", create_asgi_app)
140+
141+
create_server = pretend.call_recorder(lambda *a, **kw: server)
142+
143+
from functions_framework import _cli
144+
145+
monkeypatch.setattr(_cli, "create_server", create_server)
146+
147+
runner = CliRunner(env={"GATEWAY": "asgi"})
148+
result = runner.invoke(cli_command, ["--target", "foo"])
149+
150+
assert result.exit_code == 0
151+
assert create_asgi_app.calls == [pretend.call("foo", None, "http")]
152+
153+
154+
def test_cli_invalid_gateway():
155+
"""Test that invalid gateway values are rejected"""
156+
runner = CliRunner()
157+
result = runner.invoke(cli_command, ["--target", "foo", "--gateway", "invalid"])
158+
159+
assert result.exit_code == 2
160+
assert "Invalid value" in result.output
161+
assert "'invalid' is not one of 'wsgi', 'asgi'" in result.output

tests/test_functions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import pretend
2222
import pytest
2323

24-
# Conditional import for Starlette
2524
if sys.version_info >= (3, 8):
2625
from starlette.testclient import TestClient as StarletteTestClient
2726
else:
@@ -31,7 +30,6 @@
3130

3231
from functions_framework import LazyWSGIApp, create_app, errorhandler, exceptions
3332

34-
# Conditional import for async functionality
3533
if sys.version_info >= (3, 8):
3634
from functions_framework.aio import create_asgi_app
3735
else:

0 commit comments

Comments
 (0)