Skip to content
Open
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
74 changes: 73 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ steps.rust-version.outputs.rust-version }}
components: clippy, rustfmt

- name: Install wasm targets for generated-project checks
if: matrix.suite == 'cli'
Expand All @@ -73,6 +74,13 @@ jobs:
if: matrix.suite == 'cli'
run: cargo test -p edgezero-cli --features nested-app-config-check --bin check_no_nested_app_config

- name: Verify native request ingress harness
if: matrix.suite == 'workspace'
run: |
cargo fmt --manifest-path tests/fixtures/request-fidelity-runner/Cargo.toml --all -- --check
cargo test --manifest-path tests/fixtures/request-fidelity-runner/Cargo.toml --locked
cargo clippy --manifest-path tests/fixtures/request-fidelity-runner/Cargo.toml --all-targets --locked -- -D warnings

- name: Lifecycle fixture host tests
if: matrix.suite == 'workspace'
run: >-
Expand Down Expand Up @@ -154,7 +162,7 @@ jobs:
- adapter: spin
target: wasm32-wasip2
runner_env: CARGO_TARGET_WASM32_WASIP2_RUNNER
runner_value: wasmtime run
runner_value: wasmtime run -W component-model-async=y -S p3=y,http=y

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Why does this PR change the Spin runner flags?

The latest test.yml runs on main succeed with plain wasmtime run for the Spin contract suite, and this change also rewrites the Spin and overview guide instructions. If the P3/async/HTTP flags are required by something introduced here, could you note what in the PR description? Otherwise, please revert this line and the matching doc edits to keep the PR scoped.

steps:
- uses: actions/checkout@v6
with:
Expand Down Expand Up @@ -184,10 +192,35 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ steps.rust-version.outputs.rust-version }}
components: clippy, rustfmt

- name: Add wasm target
run: rustup target add ${{ matrix.target }}

- name: Set up matching browser and driver
if: matrix.adapter == 'cloudflare'
id: chrome
uses: browser-actions/setup-chrome@v2
with:
chrome-version: 154.0.8037.92
install-chromedriver: true
install-dependencies: true

- name: Set up Node for local Worker tooling
if: matrix.adapter == 'cloudflare'
uses: actions/setup-node@v6
with:
node-version: 24.12.0
cache: npm
cache-dependency-path: tests/fixtures/request-fidelity/package-lock.json

- name: Bind browser tests to pinned Chrome
if: matrix.adapter == 'cloudflare'
env:
CHROME_BINARY: ${{ steps.chrome.outputs.chrome-path }}
run: |
node -e 'require("node:fs").writeFileSync(require("node:path").join(process.env.RUNNER_TEMP, "edgezero-webdriver.json"), JSON.stringify({"goog:chromeOptions": {binary: process.env.CHROME_BINARY}}))'

- name: Resolve wasm-bindgen CLI version
if: matrix.adapter == 'cloudflare'
id: wasm-bindgen-version
Expand Down Expand Up @@ -278,6 +311,8 @@ jobs:

- name: Run ${{ matrix.adapter }} wasm tests
env:
CHROMEDRIVER: ${{ steps.chrome.outputs.chromedriver-path }}
WASM_BINDGEN_TEST_WEBDRIVER_JSON: ${{ runner.temp }}/edgezero-webdriver.json
${{ matrix.runner_env }}: ${{ matrix.runner_value }}
run: cargo test -p edgezero-adapter-${{ matrix.adapter }} --features ${{ matrix.adapter }} --target ${{ matrix.target }} --test contract

Expand All @@ -291,6 +326,43 @@ jobs:
${{ matrix.runner_env }}: ${{ matrix.runner_value }}
run: cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --lib

# Spin's metadata helper runs on the host; its full lib test artifact
# imports Spin-specific interfaces unavailable in plain Wasmtime.
# The real Spin HTTP fixture below executes the converter instead.
- name: Install lock-matched worker-build
if: matrix.adapter == 'cloudflare'
run: cargo install worker-build --version 0.8.3 --locked --force

- name: Install pinned Spin HTTP runtime
if: matrix.adapter == 'spin'
shell: bash
run: |
version="$(awk '$1 == "spin-cli" { gsub(/"/, "", $3); print $3; exit }' tests/fixtures/request-fidelity/Cargo.toml)"
install_dir="$RUNNER_TEMP/spin"
mkdir -p "$install_dir"
curl -fL "https://github.com/spinframework/spin/releases/download/v${version}/spin-v${version}-linux-amd64.tar.gz" -o "$RUNNER_TEMP/spin.tar.gz"
tar -xzf "$RUNNER_TEMP/spin.tar.gz" -C "$install_dir"
echo "$install_dir" >> "$GITHUB_PATH"
"$install_dir/spin" --version

- name: Check isolated fixture formatting
run: cargo fmt --manifest-path tests/fixtures/request-fidelity/Cargo.toml --all -- --check

- name: Lint isolated ingress fixture
run: >-
cargo clippy --manifest-path tests/fixtures/request-fidelity/Cargo.toml
--features ${{ matrix.adapter }} --target ${{ matrix.target }}
--all-targets --locked -- -D warnings

- name: Run local HTTP ingress probes
shell: bash
run: |
if [ "${{ matrix.adapter }}" = "cloudflare" ]; then
WASM_BINDGEN_BIN="$(command -v wasm-bindgen)"
export WASM_BINDGEN_BIN
fi
./scripts/test-request-fidelity.sh ${{ matrix.adapter }}

- name: Fastly reusable HTTP smoke
if: matrix.adapter == 'fastly'
run: ./scripts/smoke_test_reusable_app.sh --adapter fastly --suite smoke
Expand Down
169 changes: 165 additions & 4 deletions crates/edgezero-adapter-axum/src/dev_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ use std::path::{Path, PathBuf};
use std::str::FromStr as _;
use std::sync::Arc;

use crate::request::HttpTransport;
use anyhow::Context as _;
use axum::Router;
use axum::body::Body as AxumBody;
use axum::http::Request as AxumRequest;
use tokio::net::TcpListener as TokioTcpListener;
use tokio::runtime::Builder as RuntimeBuilder;
use tokio::signal;
Expand Down Expand Up @@ -299,10 +302,12 @@ async fn serve_with_stores(
}
service
};
let axum_router = Router::new().fallback_service(service_fn(move |req| {
let mut svc = service.clone();
async move { svc.call(req).await }
}));
let axum_router =
Router::new().fallback_service(service_fn(move |mut req: AxumRequest<AxumBody>| {
req.extensions_mut().insert(HttpTransport);
let mut svc = service.clone();
async move { svc.call(req).await }
}));
let make_service = axum_router.into_make_service_with_connect_info::<SocketAddr>();

let shutdown = enable_ctrl_c.then_some(async {
Expand Down Expand Up @@ -745,6 +750,23 @@ mod integration_tests {
use tokio::task::{JoinHandle, spawn_blocking};
use tokio::time::sleep;

use async_trait::async_trait;
use edgezero_core::body::Body;
use edgezero_core::http::{HeaderValue, Method, Request, Response, response_builder};
use edgezero_core::middleware::{Middleware, Next};
use edgezero_core::request::{CapturedTarget, Preservation, RequestIngress};
use edgezero_core::router::PreDispatchHook;
use std::io::{Read as _, Write as _};
use std::net::TcpStream;
use std::sync::atomic::{AtomicUsize, Ordering};

struct IngressHook {
calls: Arc<AtomicUsize>,
}
struct IngressMiddleware {
calls: Arc<AtomicUsize>,
}

struct TestServer {
_temp_dir: tempfile::TempDir,
base_url: String,
Expand All @@ -756,6 +778,145 @@ mod integration_tests {
handle: JoinHandle<()>,
}

#[async_trait(?Send)]
impl Middleware for IngressMiddleware {
async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result<Response, EdgeError> {
self.calls.fetch_add(1, Ordering::SeqCst);
next.run(ctx).await
}
}

#[async_trait(?Send)]
impl PreDispatchHook for IngressHook {
async fn handle(&self, request: &mut Request) -> Result<Option<Response>, EdgeError> {
self.calls.fetch_add(1, Ordering::SeqCst);
let ingress = request
.extensions()
.get::<RequestIngress>()
.expect("should carry adapter metadata");
assert_eq!(
ingress
.origin()
.expect("should expose HTTP binding")
.scheme(),
"http"
);
let CapturedTarget::Complete(target) = ingress.target() else {
panic!("should capture target")
};
assert_eq!(target.value(), request.uri().to_string());
match request.headers()["x-fixture"]
.to_str()
.expect("should read fixed fixture")
{
"repeat" => {
assert_eq!(request.method().as_str(), "EXAMPLE-METHOD");
assert_eq!(request.headers().get_all("origin").iter().count(), 2);
assert_eq!(request.headers().get_all("x-control").iter().count(), 2);
let cookies: Vec<_> = request
.headers()
.get_all("cookie")
.iter()
.map(HeaderValue::as_bytes)
.collect();
assert_eq!(cookies, [b"a=\xff".as_slice(), b"b=2".as_slice()]);
}
"length" => {
assert_eq!(
request.headers().get_all("content-length").iter().count(),
1
);
assert_eq!(
ingress
.header_fidelity(&"content-length".parse().expect("should parse name"))
.field_multiplicity(),
Preservation::Unknown
);
}
"te-cl" => assert!(!request.headers().contains_key("content-length")),
"continue" => {
*request.method_mut() = Method::GET;
*request.uri_mut() = "/ordinary".parse().expect("should parse fixed route");
return Ok(None);
}
"error" => return Err(EdgeError::bad_request("fixture rejection")),
"dot" => assert_eq!(request.uri().path(), "/reserved/%2e/../%2F//"),
_ => panic!("should use fixed fixture"),
}
Ok(Some(
response_builder()
.status(204)
.header("x-hook", "yes")
.body(Body::empty())
.map_err(EdgeError::internal)?,
))
}
}

#[action]
async fn ingress_ordinary() -> &'static str {
"continued"
}

fn raw_ingress_response(addr: SocketAddr, fixture: &[u8]) -> Vec<u8> {
let mut connection = TcpStream::connect(addr).expect("should connect listener");
connection
.set_read_timeout(Some(Duration::from_secs(3)))
.expect("should set timeout");
connection
.write_all(fixture)
.expect("should send exact request");
let mut response = Vec::new();
connection
.read_to_end(&mut response)
.expect("should read response");
response
}

#[tokio::test(flavor = "multi_thread")]
async fn ingress_raw_tcp_distinguishes_parser_and_hook_behaviour() {
let hook_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let router = RouterService::builder()
.middleware(IngressMiddleware {
calls: Arc::clone(&middleware_calls),
})
.pre_dispatch_hook(Arc::new(IngressHook {
calls: Arc::clone(&hook_calls),
}))
.get("/ordinary", ingress_ordinary)
.build();
let server = start_test_server(router).await;
let addr: SocketAddr = server
.base_url
.strip_prefix("http://")
.expect("should have HTTP binding")
.parse()
.expect("should parse listener address");
let cases: [(&[u8], &[u8], usize); 7] = [
(b"EXAMPLE-METHOD /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: repeat\r\nCookie: a=\xff\r\nOrigin: https://example.com\r\nX-Control: first\r\nCOOKIE: b=2\r\nOrigin: https://other.example.com\r\nX-Control: second\r\n\r\n", b"HTTP/1.1 204", 1),
(b"POST /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: length\r\nContent-Length: 0\r\nContent-Length: 0\r\n\r\n", b"HTTP/1.1 204", 1),
(b"POST /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: length\r\nContent-Length: 0\r\nContent-Length: 1\r\n\r\n", b"HTTP/1.1 400", 0),
(b"GET /reserved/%2e/../%2F// HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: dot\r\n\r\n", b"HTTP/1.1 204", 1),
(b"EXAMPLE-METHOD /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: continue\r\n\r\n", b"HTTP/1.1 200", 1),
(b"GET /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: error\r\n\r\n", b"HTTP/1.1 400", 1),
(b"POST /missing HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-Fixture: te-cl\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n0\r\n\r\n", b"HTTP/1.1 204", 1),
];
for (fixture, status, calls) in cases {
let before = hook_calls.load(Ordering::SeqCst);
let response = spawn_blocking(move || raw_ingress_response(addr, fixture))
.await
.expect("should join socket probe");
assert!(
response.starts_with(status),
"should return expected fixed fixture status"
);
assert_eq!(hook_calls.load(Ordering::SeqCst) - before, calls);
}
assert_eq!(middleware_calls.load(Ordering::SeqCst), 1);
server.handle.abort();
}

async fn start_test_server(router: RouterService) -> TestServer {
let listener = TokioTcpListener::bind("127.0.0.1:0")
.await
Expand Down
Loading
Loading