Enforce explicit targets (WEB_BASE_URL/API_BASE_URL) and prove the system is reachable
over the network by hostname in default (bridged/LAN) mode. Supports a temporary NAT recording mode
(CI_NAT_MODE=1) where localhost is an explicit exception.
Objective: Run tests only against an explicitly named, network-reachable SUT. Validate service availability locally on the SUT host and remotely from the runner via hostname.
Rule: No implicit defaults. If an env var is missing, fail fast.
Targets are explicit and logged: WEB_BASE_URL and API_BASE_URL.
Boundary rule (default): In BRIDGED/LAN mode (CI_NAT_MODE!=1),
the runner must reach the SUT by stable name (e.g., sut.testlab).
No port forwarding, no SSH tunnels, no “localhost as environment”.
Recording exception: In NAT recording mode (CI_NAT_MODE=1),
localhost is allowed because the SUT is exposed via local port-forwards (e.g., 127.0.0.1:3000, 127.0.0.1:3001).
This is temporary and must be explicit.
Success criteria:
Sprint 2 is complete. The SUT is reachable by hostname from the runner, ports are explicitly published on the SUT host, and internal dependencies are validated strictly within the SUT boundary.
sut.testlab (default mode)3000 (web) and 3001 (api) are LAN-reachable (not loopback-only) in default modemongosh ping returns { ok: 1 })# SUT host (compose + published ports)
cd ~/conduit
docker compose up -d
docker compose ps
sudo ss -lntp | grep -E '(:3000\s|:3001\s)'
# SUT host (local HTTP)
curl -I http://127.0.0.1:3000/ | sed -n '1,8p'
curl -I http://127.0.0.1:3001/ | sed -n '1,12p'
# Runner (DNS + TCP + HTTP via hostname)
dscacheutil -q host -a name sut.testlab
nc -vz sut.testlab 3000
nc -vz sut.testlab 3001
curl -sS -D- http://sut.testlab:3000/ -o /dev/null | sed -n '1,8p'
curl -sS -D- http://sut.testlab:3001/ -o /dev/null | sed -n '1,12p'
# Dependency boundary proof (SUT host only)
docker exec -it conduit-mongo mongosh --quiet --eval 'db.adminCommand({ ping: 1 })'
Sprint 2 hardens “explicit targeting” by using separate web + API targets. BASE_URL may exist in older scripts,
but Sprint 2’s contract is WEB_BASE_URL and API_BASE_URL.
# Default (BRIDGED/LAN)
export CI_NAT_MODE="0"
export WEB_BASE_URL="http://sut.testlab:3000"
export API_BASE_URL="http://sut.testlab:3001/api"
# Temporary (NAT recording mode)
export CI_NAT_MODE="1"
export WEB_BASE_URL="http://127.0.0.1:3000"
export API_BASE_URL="http://127.0.0.1:3001/api"
# Optional (compatibility only)
export BASE_URL="${WEB_BASE_URL}"
Guardrail: if CI_NAT_MODE!=1, targets must not contain localhost or 127.0.0.1.
Fail immediately if targets are missing or invalid for the selected mode.
: "${WEB_BASE_URL:?missing WEB_BASE_URL}"
: "${API_BASE_URL:?missing API_BASE_URL}"
echo "CI_NAT_MODE=${CI_NAT_MODE:-0}"
echo "WEB_BASE_URL=${WEB_BASE_URL}"
echo "API_BASE_URL=${API_BASE_URL}"
if [ "${CI_NAT_MODE:-0}" != "1" ]; then
echo "${WEB_BASE_URL}" | grep -Eqi 'localhost|127\.0\.0\.1|0\.0\.0\.0' && { echo "FAIL: WEB_BASE_URL is localhost in default mode"; exit 1; }
echo "${API_BASE_URL}" | grep -Eqi 'localhost|127\.0\.0\.1|0\.0\.0\.0' && { echo "FAIL: API_BASE_URL is localhost in default mode"; exit 1; }
fi
Prove the stack is up and ports are published.
cd ~/conduit
docker compose up -d
docker compose ps
# verify listeners for expected ports
sudo ss -lntp | grep -E '(:3000\s|:3001\s)' || echo "NO LISTENER 3000/3001"
cd ~/conduit
docker compose pull
docker compose up -d --build --remove-orphans
docker compose down
Validate service response from the SUT host using the published ports.
curl -I http://127.0.0.1:3000/ | sed -n '1,8p'
curl -I http://127.0.0.1:3001/ | sed -n '1,12p'
Default mode requires hostname-based reachability. This is where “no port forwarding” is enforced.
# Derive host from WEB_BASE_URL
SUT_HOST="$(echo "$WEB_BASE_URL" | sed -E 's#^https?://([^:/]+).*#\1#')"
echo "SUT_HOST=${SUT_HOST}"
# DNS proof (runner)
dscacheutil -q host -a name "${SUT_HOST}"
# TCP proof (runner)
nc -vz "${SUT_HOST}" 3000
nc -vz "${SUT_HOST}" 3001
# HTTP proof (runner) — must be by hostname in default mode
curl -sS -D- "http://${SUT_HOST}:3000/" -o /dev/null | sed -n '1,8p'
curl -sS -D- "http://${SUT_HOST}:3001/" -o /dev/null | sed -n '1,12p'
Use this only when the SUT is operating in NAT mode and exposed via localhost port-forwards for recording safety. In this mode, runner reachability by hostname is not the goal; the explicit goal is “localhost is intentionally used and works.”
export CI_NAT_MODE="1"
export WEB_BASE_URL="http://127.0.0.1:3000"
export API_BASE_URL="http://127.0.0.1:3001/api"
# Boundary proof flips: localhost must respond
curl -fsS --max-time 5 "http://127.0.0.1:3000/" >/dev/null \
&& echo "PASS: localhost returned HTTP (NAT recording mode)" \
|| { echo "FAIL: localhost did not respond in NAT mode"; exit 1; }
# TCP proof (localhost)
nc -vz 127.0.0.1 3000
nc -vz 127.0.0.1 3001
# API identity proof
curl -sS -D- "http://127.0.0.1:3001/api/tags" -o /dev/null | sed -n '1,12p'
Validate internal dependencies from the SUT without exposing them to the runner.
docker exec -it conduit-mongo mongosh --quiet --eval 'db.adminCommand({ ping: 1 })'
docker exec -it conduit-mongo mongosh --quiet --eval '
db = db.getSiblingDB("conduit");
db.getCollectionNames();
'
Public rule: outputs must be sanitized. Private blocks may contain actuals.
| Explicit targeting | WEB_BASE_URL and API_BASE_URL printed in logs; missing vars fail fast. |
| docker compose ps | Containers show Up state. |
| Port listening (default) | SUT host shows listeners on LAN interface / 0.0.0.0:3000 and 0.0.0.0:3001 (not loopback-only). |
| Remote HTTP (default) | Runner receives HTTP/1.1 status lines from sut.testlab endpoints. |
| Localhost HTTP (NAT mode) | When CI_NAT_MODE=1, localhost checks succeed for 127.0.0.1:3000/3001. |
| DB ping (SUT) | Mongo returns { ok: 1 }. |
Symptom: vars are blank, or default mode is accidentally pointing to localhost.
Action: verify the fail-fast block and the mode value.
Symptom: SUT is healthy locally; runner cannot reach it.
Diagnosis (Runner):
nc -vz sut.testlab 3000
nc -vz sut.testlab 3001
Action: check firewall controls, port publishing (not loopback-only), and network segmentation.
Symptom: curl to 127.0.0.1:3000 times out/refuses.
Likely cause: port-forward missing, VM stopped, or services down.
Action: verify VirtualBox forwards and SUT service health.
# keep raw IPs private
# route/arp, firewall rules, and host NIC checks
Availability validation reduces uncertainty before test execution. Target discipline ensures failures reflect real service or network conditions rather than incomplete startup or local substitution.
Default mode proves production-real reachability by stable name across the network boundary. NAT recording mode is a temporary, explicit exception used to avoid exposing LAN details during recordings.