folk-runtime-embed 0.1.6

Embedded PHP runtime for Folk — PHP interpreter runs in-process via FFI
Documentation
//! Tests for the custom Folk SAPI module.
//!
//! Verifies that HTTP request data is properly passed to PHP:
//! - $_SERVER['REQUEST_METHOD'], REQUEST_URI, QUERY_STRING
//! - $_GET populated from query string
//! - $_POST populated from request body
//! - $_COOKIE populated from cookie header
//! - header() calls captured by Rust
//! - echo output captured as response body
//!
//! IMPORTANT: PHP embed SAPI is NOT thread-safe for init/shutdown.
//! All tests run sequentially (single-threaded).

use folk_runtime_embed::php::{PhpInstance, RequestContext};

#[test]
fn test_custom_sapi_boot_and_eval() {
    let mut php = PhpInstance::boot_custom_sapi().expect("custom SAPI boot failed");

    let mut ctx = RequestContext::new("GET", "/test");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo "hello from custom SAPI";"#)
        .expect("eval failed");
    assert_eq!(result.output, "hello from custom SAPI");

    php.request_shutdown();
}

#[test]
fn test_server_request_method() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("POST", "/api/users");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo $_SERVER['REQUEST_METHOD'];"#)
        .expect("eval failed");
    assert_eq!(result.output, "POST");

    php.request_shutdown();
}

#[test]
fn test_server_request_uri() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("GET", "/path/to/resource?foo=bar");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo $_SERVER['REQUEST_URI'];"#)
        .expect("eval failed");
    assert_eq!(result.output, "/path/to/resource?foo=bar");

    php.request_shutdown();
}

#[test]
fn test_server_query_string() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("GET", "/search?q=test&page=2").query_string("q=test&page=2");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo $_SERVER['QUERY_STRING'];"#)
        .expect("eval failed");
    assert_eq!(result.output, "q=test&page=2");

    php.request_shutdown();
}

#[test]
fn test_get_superglobal() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("GET", "/search?name=folk&version=1")
        .query_string("name=folk&version=1");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo $_GET['name'] . ':' . $_GET['version'];"#)
        .expect("eval failed");
    assert_eq!(result.output, "folk:1");

    php.request_shutdown();
}

#[test]
fn test_post_body() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let body = b"username=admin&password=secret";
    let mut ctx = RequestContext::new("POST", "/login")
        .content_type("application/x-www-form-urlencoded")
        .body(body);
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo $_POST['username'] . ':' . $_POST['password'];"#)
        .expect("eval failed");
    assert_eq!(result.output, "admin:secret");

    php.request_shutdown();
}

#[test]
fn test_raw_post_body() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let json_body = br#"{"key":"value"}"#;
    let mut ctx = RequestContext::new("POST", "/api")
        .content_type("application/json")
        .body(json_body);
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo file_get_contents('php://input');"#)
        .expect("eval failed");
    assert_eq!(result.output, r#"{"key":"value"}"#);

    php.request_shutdown();
}

#[test]
fn test_cookie_superglobal() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("GET", "/").cookie("session_id=abc123; theme=dark");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo $_COOKIE['session_id'] . ':' . $_COOKIE['theme'];"#)
        .expect("eval failed");
    assert_eq!(result.output, "abc123:dark");

    php.request_shutdown();
}

#[test]
fn test_http_headers_to_server() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("GET", "/")
        .header("Host", "example.com")
        .header("Accept", "text/html")
        .header("X-Custom-Header", "custom-value");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo $_SERVER['HTTP_HOST'];"#)
        .expect("eval failed");
    assert_eq!(result.output, "example.com");

    let result = php
        .eval(r#"echo $_SERVER['HTTP_ACCEPT'];"#)
        .expect("eval failed");
    // Output accumulates within request, clear it
    assert!(result.output.contains("text/html"));

    let result = php
        .eval(r#"echo $_SERVER['HTTP_X_CUSTOM_HEADER'];"#)
        .expect("eval failed");
    assert!(result.output.contains("custom-value"));

    php.request_shutdown();
}

#[test]
fn test_response_header_capture() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("GET", "/");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    php.eval(
        r#"
        header('Content-Type: application/json');
        header('X-Custom: test-value');
        echo '{"status":"ok"}';
    "#,
    )
    .expect("eval failed");

    let response = php.take_response();
    assert_eq!(response.body, r#"{"status":"ok"}"#);

    // Check response headers contain our custom headers
    let has_content_type = response
        .headers
        .iter()
        .any(|h| h.contains("Content-Type") && h.contains("application/json"));
    let has_custom = response
        .headers
        .iter()
        .any(|h| h.contains("X-Custom") && h.contains("test-value"));

    assert!(
        has_content_type,
        "missing Content-Type header in: {:?}",
        response.headers
    );
    assert!(
        has_custom,
        "missing X-Custom header in: {:?}",
        response.headers
    );

    php.request_shutdown();
}

#[test]
fn test_response_status_code() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("GET", "/not-found");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    php.eval(
        r#"
        http_response_code(404);
        echo 'Not Found';
    "#,
    )
    .expect("eval failed");

    let response = php.take_response();
    assert_eq!(response.status_code, 404);
    assert_eq!(response.body, "Not Found");

    php.request_shutdown();
}

#[test]
fn test_multiple_requests_custom_sapi() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    for i in 0..50 {
        let uri = format!("/req/{i}");
        let mut ctx = RequestContext::new("GET", &uri);
        php.set_request_context(&mut ctx);
        php.request_startup().expect("request startup failed");

        let result = php
            .eval(r#"echo $_SERVER['REQUEST_URI'];"#)
            .expect("eval failed");
        assert_eq!(result.output, uri);

        php.request_shutdown();
    }
}

#[test]
fn test_server_info() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("GET", "/")
        .server("localhost", 8080)
        .protocol("HTTP/1.1");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php
        .eval(r#"echo $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT'] . ' ' . $_SERVER['SERVER_PROTOCOL'];"#)
        .expect("eval failed");
    assert_eq!(result.output, "localhost:8080 HTTP/1.1");

    php.request_shutdown();
}

#[test]
fn test_sapi_name() {
    let mut php = PhpInstance::boot_custom_sapi().expect("boot failed");

    let mut ctx = RequestContext::new("GET", "/");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("request startup failed");

    let result = php.eval(r#"echo php_sapi_name();"#).expect("eval failed");
    assert_eq!(result.output, "folk-embed");

    php.request_shutdown();
}