saddle-framework 0.3.15

The single business-facing facade for Saddle applications
use super::*;
use std::io::{Read, Write};

#[test]
#[ignore = "isolated real process launched by ingress_token_process"]
fn ingress_token_child() {
    let path = PathBuf::from(std::env::var_os("SADDLE_TOKEN_TEST_CONFIG").unwrap());
    let config = ProcessConfig::<()>::load(&path).unwrap();
    let (app, listener) = saddle_core::BootstrapRendezvousIssuer::issue()
        .freeze_application(saddle_core::GeneratedApplicationFreezeSource::new(
            "token-test",
            b"token-test",
            &["token.call"],
        ))
        .unwrap();
    coordinate_profusegw_process(
        config,
        saddle_boundary::ingress::ProfuseGwListenerAdapter::new("token-test").unwrap(),
        app,
        listener,
        |_| async { Ok(()) },
        move |_, (), _, _| {
            std::fs::write(path.with_file_name("handler-called"), b"called").unwrap();
            async {
                Ok(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}".to_vec())
            }
        },
    )
    .unwrap();
}

fn address() -> SocketAddr {
    std::net::TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap()
}

struct Child {
    process: std::process::Child,
    root: PathBuf,
}
impl Drop for Child {
    fn drop(&mut self) {
        let _ = self.process.kill();
        let _ = self.process.wait();
        let _ = std::fs::remove_dir_all(&self.root);
    }
}

fn request(address: SocketAddr, bytes: &[u8]) -> Vec<u8> {
    let mut socket = std::net::TcpStream::connect(address).unwrap();
    socket
        .set_read_timeout(Some(Duration::from_secs(2)))
        .unwrap();
    socket.write_all(bytes).unwrap();
    let mut response = Vec::new();
    socket.read_to_end(&mut response).unwrap();
    response
}

#[test]
fn ingress_token_process() {
    for configured in [true, false] {
        let business = address();
        let management = address();
        let nonce = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let root = std::env::temp_dir().join(format!(
            "saddle-ingress-token-{}-{nonce}",
            std::process::id()
        ));
        std::fs::create_dir(&root).unwrap();
        let path = root.join("saddle.toml");
        let auth = if configured {
            "[framework.ingress]\nprofusegw_token=\"ingress-only\"\n"
        } else {
            ""
        };
        std::fs::write(&path, format!("[framework]\nlisten=\"{business}\"\n{auth}[framework.management]\nbind=\"{management}\"\n[framework.admission]\ncpuCores=2\nmemoryMb=512\n[framework.admission.dependencies]\ndatabaseConcurrency=1\nprofusecontractConcurrency=1\n[framework.profusecontract]\nauthority=\"http://localhost:50051\"\ntoken=\"outbound-only\"\n[framework.observability.logging]\ndirectory=\"{}\"\n[secrets]\n",root.join("logs").display())).unwrap();
        let output = std::fs::File::create(root.join("child.log")).unwrap();
        let mut child = Child {
            process: std::process::Command::new(std::env::current_exe().unwrap())
                .args([
                    "--exact",
                    "process::token_tests::ingress_token_child",
                    "--ignored",
                    "--nocapture",
                ])
                .env("SADDLE_TOKEN_TEST_CONFIG", &path)
                .stdout(output.try_clone().unwrap())
                .stderr(output)
                .spawn()
                .unwrap(),
            root,
        };
        let mut started = false;
        for _ in 0..200 {
            if std::net::TcpStream::connect(management).is_ok() {
                started = true;
                break;
            }
            assert!(
                child.process.try_wait().unwrap().is_none(),
                "{}",
                std::fs::read_to_string(child.root.join("child.log")).unwrap()
            );
            std::thread::sleep(Duration::from_millis(10));
        }
        assert!(started);
        for endpoint in ["live", "ready", "metrics"] {
            let response = request(
                management,
                format!("GET /{endpoint} HTTP/1.1\r\nHost: localhost\r\n\r\n").as_bytes(),
            );
            assert!(response.starts_with(b"HTTP/1.1 200"));
        }
        if configured {
            for header in [
                "",
                "Authorization: Bearer ingress-only\r\n",
                "X-ProfuseGW-Token: outbound-only\r\n",
                "X-ProfuseGW-Token: ingress-only\r\nx-profusegw-token: ingress-only\r\n",
            ] {
                let response = request(business, format!("POST /saddle/v1/ingress/profusegw/invoke HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: 1024\r\nX-Request-Id: r1\r\nX-Call-Id: c1\r\n{header}\r\n").as_bytes());
                assert!(response.starts_with(b"HTTP/1.1 401"));
                assert!(!child.root.join("handler-called").exists());
            }
        }
        let body = r#"{"target":{"app":"token-test","interfaceId":"token.call"},"profuseGwContext":{"userInfo":{"userId":"u1"},"traceInfo":{"traceId":"trace-1","rpcId":"0"},"ldcInfo":{"zone":"z1","idc":"i1","env":"test"}},"requestData":{}}"#;
        let header = if configured {
            "x-PrOfUsEgW-ToKeN: ingress-only\r\n"
        } else {
            ""
        };
        let response = request(business, format!("POST /saddle/v1/ingress/profusegw/invoke HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\nX-Request-Id: r2\r\nX-Call-Id: c2\r\n{header}\r\n{body}",body.len()).as_bytes());
        assert!(
            response.starts_with(b"HTTP/1.1 200"),
            "{}",
            String::from_utf8_lossy(&response)
        );
        assert!(child.root.join("handler-called").exists());
        assert!(
            std::process::Command::new("kill")
                .args(["-TERM", &child.process.id().to_string()])
                .status()
                .unwrap()
                .success()
        );
        let mut ended = false;
        for _ in 0..300 {
            if let Some(exit) = child.process.try_wait().unwrap() {
                assert!(exit.success());
                ended = true;
                break;
            }
            std::thread::sleep(Duration::from_millis(10));
        }
        assert!(ended);
    }
}