@@ -2,12 +2,17 @@ defmodule PtcRunnerMcp.Http.ServerSmokeTest do
22 @ moduledoc """
33 Integration smoke test for the HTTP transport bootstrap.
44
5- Boots `PtcRunnerMcp.Http.Server` for real on an OS-assigned ephemeral
6- port (port 0), issues one real HTTP request over a TCP socket, asserts
7- the server serves it, then lets `start_supervised!/1` tear it down
8- cleanly. This exercises the production `Server.child_spec/1` Bandit glue
9- and the `PlugWithConfig` -> `Router` dispatch path end to end (rather
10- than calling `Router.call/2` in-process like the unit-style tests).
5+ Boots `PtcRunnerMcp.Http.Server` for real on a free OS-assigned port,
6+ issues one real HTTP request over a TCP socket, asserts the server
7+ serves it, then lets `start_supervised!/1` tear it down cleanly. This
8+ exercises the production `Server.child_spec/1` Bandit glue and the
9+ `PlugWithConfig` -> `Router` dispatch path end to end (rather than
10+ calling `Router.call/2` in-process like the unit-style tests).
11+
12+ The port is acquired from the OS up front via `free_port/0` rather than
13+ passing `0`: `Config.resolve/1` rejects a non-positive `http_port` and
14+ would silently fall back to the default fixed port, which could collide
15+ with a real listener in CI.
1116 """
1217 use ExUnit.Case , async: false
1318
@@ -19,13 +24,13 @@ defmodule PtcRunnerMcp.Http.ServerSmokeTest do
1924 # required so `Config.resolve/1` succeeds and the bind host validates.
2025 @ token String . duplicate ( "a" , 32 )
2126
22- # Boot the real transport on an ephemeral port and return its bound
23- # TCP port. The server is supervised so ExUnit tears it (and its
27+ # Boot the real transport on a free OS-assigned port and return its
28+ # bound TCP port. The server is supervised so ExUnit tears it (and its
2429 # listening socket) down at test end without leaking processes/ports.
2530 defp boot_server! ( config_args ) do
2631 { :ok , cfg } =
2732 HttpConfig . resolve (
28- Map . merge ( % { http: true , http_auth_token: @ token , http_port: 0 } , config_args )
33+ Map . merge ( % { http: true , http_auth_token: @ token , http_port: free_port ( ) } , config_args )
2934 )
3035
3136 # Drive the production child-spec builder, not a hand-rolled Bandit
@@ -39,7 +44,17 @@ defmodule PtcRunnerMcp.Http.ServerSmokeTest do
3944 { cfg , port }
4045 end
4146
42- describe "transport bootstrap on an ephemeral port" do
47+ # Ask the OS for an unused loopback TCP port, then release it so the
48+ # server can bind it. The brief window between close and re-bind is the
49+ # standard trade-off for test servers and is safe here (async: false).
50+ defp free_port do
51+ { :ok , socket } = :gen_tcp . listen ( 0 , ip: { 127 , 0 , 0 , 1 } , reuseaddr: true )
52+ { :ok , port } = :inet . port ( socket )
53+ :ok = :gen_tcp . close ( socket )
54+ port
55+ end
56+
57+ describe "transport bootstrap on a free OS-assigned port" do
4358 test "serves an unauthenticated GET /health over a real socket" do
4459 { _cfg , port } = boot_server! ( % { } )
4560 assert is_integer ( port ) and port > 0
@@ -56,7 +71,7 @@ defmodule PtcRunnerMcp.Http.ServerSmokeTest do
5671 # parse_ip/1 turned the string host into an inet tuple for Bandit.
5772 assert cfg . host == "127.0.0.1"
5873
59- # The OS assigned a concrete port (config asked for 0) .
74+ # The server bound the free port we acquired from the OS .
6075 assert port != 0
6176
6277 resp = Req . get! ( "http://127.0.0.1:#{ port } /health" , retry: false )
0 commit comments