From ac612b2fa94dfa8ba6e054893bd2589ed243ca97 Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Thu, 5 Mar 2026 16:02:30 +0530 Subject: [PATCH 01/15] Wip integration testing with testcontainers --- Cargo.lock | 2100 ++++++++++++++++- Cargo.toml | 3 +- INTEGRATION_TESTS_PLAN.md | 1966 +++++++++++++++ crates/fastly/src/main.rs | 3 + crates/integration-tests/Cargo.toml | 22 + .../tests/common/assertions.rs | 276 +++ .../integration-tests/tests/common/config.rs | 91 + crates/integration-tests/tests/common/mod.rs | 7 + .../integration-tests/tests/common/runtime.rs | 122 + 9 files changed, 4491 insertions(+), 99 deletions(-) create mode 100644 INTEGRATION_TESTS_PLAN.md create mode 100644 crates/integration-tests/Cargo.toml create mode 100644 crates/integration-tests/tests/common/assertions.rs create mode 100644 crates/integration-tests/tests/common/config.rs create mode 100644 crates/integration-tests/tests/common/mod.rs create mode 100644 crates/integration-tests/tests/common/runtime.rs diff --git a/Cargo.lock b/Cargo.lock index 67fe9e7f..0404a7a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -69,6 +82,44 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "astral-tokio-tar" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" +dependencies = [ + "filetime", + "futures-core", + "libc", + "portable-atomic", + "rustc-hash", + "tokio", + "tokio-stream", + "xattr", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -80,18 +131,73 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base16ct" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -137,6 +243,83 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" +dependencies = [ + "async-stream", + "base64 0.22.1", + "bitflags 2.10.0", + "bollard-buildkit-proto", + "bollard-stubs", + "bytes", + "chrono", + "futures-core", + "futures-util", + "hex", + "home", + "http", + "http-body-util", + "hyper", + "hyper-named-pipe", + "hyper-rustls", + "hyper-util", + "hyperlocal", + "log", + "num", + "pin-project-lite", + "rand 0.9.2", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-buildkit-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-prost", + "ureq", +] + +[[package]] +name = "bollard-stubs" +version = "1.49.1-rc.28.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623" +dependencies = [ + "base64 0.22.1", + "bollard-buildkit-proto", + "bytes", + "chrono", + "prost", + "serde", + "serde_json", + "serde_repr", + "serde_with", +] + [[package]] name = "brotli" version = "8.0.2" @@ -170,6 +353,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -225,6 +414,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -255,7 +445,7 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml", + "toml 0.9.8", "winnow", "yaml-rust2", ] @@ -305,6 +495,26 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -342,7 +552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -354,10 +564,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "cssparser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.11.3", + "smallvec", +] + [[package]] name = "cssparser" version = "0.36.0" @@ -367,7 +590,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf", + "phf 0.13.1", "smallvec", ] @@ -414,8 +637,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -432,13 +665,38 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.111", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.111", ] @@ -478,7 +736,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.111", @@ -494,13 +752,45 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] + [[package]] name = "derive_more" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ - "derive_more-impl", + "derive_more-impl 2.0.1", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "unicode-xid", ] [[package]] @@ -556,6 +846,17 @@ dependencies = [ "const-random", ] +[[package]] +name = "docker_credential" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -577,6 +878,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ed25519" version = "2.2.3" @@ -595,13 +902,19 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2 0.10.9", "subtle", "zeroize", ] +[[package]] +name = "ego-tree" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6ba7d4eec39eaa9ab24d44a0e73a7949a1095a8b3f3abb11eddf27dbb56a53" + [[package]] name = "either" version = "1.15.0" @@ -620,7 +933,7 @@ dependencies = [ "ff", "generic-array", "group", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -632,7 +945,7 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9abf33c656a7256451ebb7d0082c5a471820c31269e49d807c538c252352186e" dependencies = [ - "indexmap", + "indexmap 2.12.1", "stable_deref_trait", ] @@ -675,7 +988,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-stack" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe413319145d1063f080f27556fd30b1d70b01e2ba10c2a6e40d4be982ffc5d1" +dependencies = [ + "anyhow", + "rustc_version", ] [[package]] @@ -688,6 +1011,17 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "etcetera" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.59.0", +] + [[package]] name = "fastly" version = "0.11.12" @@ -770,7 +1104,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -780,6 +1114,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.5" @@ -814,6 +1159,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -823,6 +1183,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.32" @@ -911,6 +1281,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -923,10 +1302,19 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.2.16" +name = "getopts" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -952,10 +1340,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "handlebars" version = "6.4.0" @@ -972,6 +1379,12 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1022,6 +1435,27 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + [[package]] name = "http" version = "1.4.0" @@ -1032,6 +1466,164 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -1164,6 +1756,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -1172,6 +1775,8 @@ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1183,6 +1788,38 @@ dependencies = [ "generic-array", ] +[[package]] +name = "integration-tests" +version = "0.1.0" +dependencies = [ + "derive_more 1.0.0", + "error-stack 0.5.0", + "log", + "reqwest", + "scraper", + "serde", + "tempfile", + "testcontainers", + "tokio", + "toml 0.8.23", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1266,9 +1903,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" @@ -1276,6 +1913,18 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.10.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1322,17 +1971,54 @@ checksum = "630bc32f75a59df2d35a9f13224b057a1c1ff1d187dfdc56190e9a54c4127917" dependencies = [ "bitflags 2.10.0", "cfg-if", - "cssparser", + "cssparser 0.36.0", "encoding_rs", "foldhash 0.2.0", "hashbrown 0.16.1", "memchr", "mime", "precomputed-hash", - "selectors", + "selectors 0.33.0", "thiserror 2.0.17", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "matchit" version = "0.9.0" @@ -1361,12 +2047,64 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1378,11 +2116,20 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -1424,6 +2171,17 @@ dependencies = [ "num-modular", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1447,34 +2205,78 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] -name = "ordered-multimap" -version = "0.7.3" +name = "openssl" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "dlv-list", - "hashbrown 0.14.5", + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", ] [[package]] -name = "p256" -version = "0.13.2" +name = "openssl-macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "elliptic-curve", - "primeorder", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] -name = "p384" -version = "0.13.1" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "elliptic-curve", - "primeorder", -] +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "elliptic-curve", + "primeorder", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "elliptic-curve", + "primeorder", +] [[package]] name = "parking_lot" @@ -1494,11 +2296,36 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.111", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -1554,25 +2381,55 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros", - "phf_shared", + "phf_macros 0.13.1", + "phf_shared 0.13.1", "serde", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + [[package]] name = "phf_codegen" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", ] [[package]] @@ -1582,7 +2439,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -1591,13 +2461,22 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.111", ] +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "phf_shared" version = "0.13.1" @@ -1607,12 +2486,38 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs1" version = "0.7.5" @@ -1634,6 +2539,18 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "poly1305" version = "0.8.0" @@ -1645,6 +2562,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1715,6 +2638,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.42" @@ -1737,8 +2692,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1748,7 +2713,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1760,6 +2735,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1769,6 +2753,35 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "regex" version = "1.12.3" @@ -1798,6 +2811,62 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "ron" version = "0.12.0" @@ -1825,7 +2894,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1867,64 +2936,211 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "rustls" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] [[package]] -name = "ryu" -version = "1.0.20" +name = "rustls-native-certs" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "rustls-pemfile" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] [[package]] -name = "sec1" -version = "0.7.3" +name = "rustls-pki-types" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "base16ct", - "der", - "generic-array", - "subtle", "zeroize", ] [[package]] -name = "selectors" -version = "0.33.0" +name = "rustls-webpki" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ - "bitflags 2.10.0", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "semver" -version = "1.0.27" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e749d29b2064585327af5038a5a8eb73aeebad4a3472e83531a436563f7208" +dependencies = [ + "ahash", + "cssparser 0.34.0", + "ego-tree", + "getopts", + "html5ever", + "precomputed-hash", + "selectors 0.26.0", + "tendril", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +dependencies = [ + "bitflags 2.10.0", + "cssparser 0.34.0", + "derive_more 0.99.20", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.11.3", + "phf_codegen 0.11.3", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" +dependencies = [ + "bitflags 2.10.0", + "cssparser 0.36.0", + "derive_more 2.0.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" @@ -1992,6 +3208,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.3" @@ -2013,6 +3238,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.1", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "servo_arc" version = "0.4.3" @@ -2059,7 +3315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -2086,6 +3342,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "spin" version = "0.9.8" @@ -2108,12 +3374,60 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.111", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2142,6 +3456,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2153,6 +3476,27 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "temp-env" version = "0.3.6" @@ -2162,6 +3506,59 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "testcontainers" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3ac71069f20ecfa60c396316c283fbf35e6833a53dff551a31b5458da05edc" +dependencies = [ + "astral-tokio-tar", + "async-trait", + "bollard", + "bytes", + "docker_credential", + "either", + "etcetera", + "futures", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "ulid", + "url", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2259,8 +3656,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", + "libc", + "mio", "pin-project-lite", + "socket2", "tokio-macros", + "windows-sys 0.61.2", ] [[package]] @@ -2274,6 +3675,26 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -2296,21 +3717,55 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + [[package]] name = "toml" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap", + "indexmap 2.12.1", "serde_core", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -2320,6 +3775,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.12.1", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_parser" version = "1.0.4" @@ -2329,27 +3798,153 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +[[package]] +name = "tonic" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 2.12.1", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "trusted-server-common" version = "0.1.0" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "brotli", "bytes", "chacha20poly1305", "chrono", "config", "cookie", - "derive_more", + "derive_more 2.0.1", "ed25519-dalek", - "error-stack", + "error-stack 0.6.0", "fastly", "flate2", "futures", @@ -2361,10 +3956,10 @@ dependencies = [ "log", "log-fastly", "lol_html", - "matchit", + "matchit 0.9.0", "once_cell", "pin-project-lite", - "rand", + "rand 0.8.5", "regex", "serde", "serde_json", @@ -2372,7 +3967,7 @@ dependencies = [ "temp-env", "tokio", "tokio-test", - "toml", + "toml 0.9.8", "trusted-server-js", "url", "urlencoding", @@ -2385,7 +3980,7 @@ name = "trusted-server-fastly" version = "0.1.0" dependencies = [ "chrono", - "error-stack", + "error-stack 0.6.0", "fastly", "fern", "futures", @@ -2407,6 +4002,12 @@ dependencies = [ "which", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typeid" version = "1.0.3" @@ -2425,6 +4026,16 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.2", + "web-time", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -2437,6 +4048,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2453,6 +4070,39 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" +dependencies = [ + "base64 0.22.1", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf-8", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -2463,6 +4113,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -2471,6 +4122,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2510,7 +4167,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" dependencies = [ - "darling", + "darling 0.20.11", "once_cell", "proc-macro-error2", "proc-macro2", @@ -2518,12 +4175,27 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2552,6 +4224,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.106" @@ -2584,6 +4269,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "8.0.0" @@ -2595,6 +4300,28 @@ dependencies = [ "winsafe", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -2636,6 +4363,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -2654,6 +4392,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2663,6 +4428,135 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" @@ -2693,6 +4587,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yaml-rust2" version = "0.10.4" diff --git a/Cargo.toml b/Cargo.toml index 032cbaa7..571491e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,9 @@ resolver = "2" members = [ "crates/common", - "crates/fastly", + "crates/fastly", "crates/js", + "crates/integration-tests", ] # Build defaults exclude the web-only tsjs crate, which is compiled via wasm-pack. diff --git a/INTEGRATION_TESTS_PLAN.md b/INTEGRATION_TESTS_PLAN.md new file mode 100644 index 00000000..b7153c33 --- /dev/null +++ b/INTEGRATION_TESTS_PLAN.md @@ -0,0 +1,1966 @@ +# Integration Tests Plan: Multi-Framework Frontend Testing + +## Revision History + +**Latest Update**: Staff engineering review applied - addressed critical architectural and implementation issues. + +### Changes Applied + +**Critical Fixes**: +1. ✅ **Trait Object Safety**: Changed from `&[&dyn FrontendFramework]` to function registry pattern to avoid static initialization issues +2. ✅ **Complete Error Types**: Added full `TestError` enum with all necessary variants +3. ✅ **Health Check Strategy**: Documented `/health` endpoint requirement and implemented fallback strategy +4. ✅ **Safe TOML Mutation**: Fixed config generation to use safe table lookups instead of panic-prone indexing +5. ✅ **WordPress Container Setup**: Clarified WordPress database requirements and recommended approach +6. ✅ **Container Build Process**: Documented Docker image pre-build strategy for CI and local development + +**Major Improvements**: +7. ✅ **WASM Binary Path**: Made configurable via environment variable +8. ✅ **Error Context**: Added comprehensive error context propagation with framework/scenario information +9. ✅ **Container Timeouts**: Added 60s timeout on container operations to prevent infinite hangs +10. ✅ **Test Isolation**: Documented sequential execution requirement and future enhancement plans + +**Documentation Additions**: +- Troubleshooting guide with common issues and solutions +- Implementation notes and best practices section +- Future enhancements roadmap (Phase 6+) +- CI workflow example with proper build steps +- Debug mode instructions + +**Estimated Impact**: Prevents 10-15 hours of debugging during implementation by addressing issues upfront. + +--- + +## Context + +The trusted server currently has 465 passing Rust unit tests and 256 JS tests, but lacks end-to-end integration tests that verify the complete system behavior against real frontend applications and across different runtime platforms. This plan implements an extensible integration test framework with dual abstractions: + +1. **Frontend Framework Abstraction**: Test against WordPress, Next.js, and future frameworks +2. **Runtime Environment Abstraction**: Test on Fastly Compute (initially), with architecture to support future platforms + +**Why this matters**: +- **Unit tests** verify individual components in isolation +- **Integration tests** prove the system works end-to-end with real frontend frameworks +- **Multi-platform architecture** enables future platform migration validation (Cloudflare Workers, etc.) + +This catches issues like HTML streaming bugs, RSC Flight format handling, GDPR signal propagation, and platform-specific runtime differences that only manifest when all components interact. + +**Initial Implementation Scope**: +- Start with **Fastly/Viceroy only** (2 frameworks × 1 runtime = 2 test combinations) +- Architecture supports adding platforms later via `RuntimeEnvironment` trait +- Future platforms (Cloudflare, Fermyon Spin, etc.) can be added without refactoring + +**Extensibility goals**: + +*Frontend Framework*: Adding support for a new framework should require only: +1. Creating a new fixture directory (`fixtures/frameworks//`) +2. Implementing the `FrontendFramework` trait (~50 lines) +3. Registering in `FRAMEWORKS` constant (~1 line) + +*Runtime Environment*: Adding support for a new platform should require only: +1. Creating a platform config template (`fixtures/configs/-template.toml`) +2. Implementing the `RuntimeEnvironment` trait (~100 lines) +3. Registering in `RUNTIME_ENVIRONMENTS` constant (~1 line) + +No changes to core test infrastructure, assertions, or test runner logic when extending either dimension. + +--- + +## Implementation Checklist + +Track progress for each phase using this checklist: + +### Phase 0: Prerequisites (BLOCKER) ⏱️ 1-2 hours ✅ COMPLETE +- [x] Add `/health` endpoint to [`crates/fastly/src/main.rs`](crates/fastly/src/main.rs) +- [x] Build WASM binary: `cargo build --target wasm32-wasip1 --release` +- [x] Start Viceroy manually to test health endpoint +- [x] Verify `curl http://127.0.0.1:7676/health` returns 200 OK + +### Phase 1: Core Infrastructure ⏱️ 4-6 hours +- [ ] Add `integration-tests` to workspace members in root `Cargo.toml` +- [ ] Create [`crates/integration-tests/Cargo.toml`](crates/integration-tests/Cargo.toml) with dependencies +- [ ] Create [`tests/common/mod.rs`](crates/integration-tests/tests/common/mod.rs) (re-exports) +- [ ] Define `RuntimeEnvironment` trait in [`tests/common/runtime.rs`](crates/integration-tests/tests/common/runtime.rs) +- [ ] Implement `RuntimeConfig` struct with platform-agnostic interface +- [ ] Create [`tests/common/config.rs`](crates/integration-tests/tests/common/config.rs) for config generation +- [ ] Implement assertion helpers in [`tests/common/assertions.rs`](crates/integration-tests/tests/common/assertions.rs) +- [ ] Create [`fixtures/configs/fastly-template.toml`](crates/integration-tests/fixtures/configs/fastly-template.toml) +- [ ] Verify: `cargo check -p integration-tests` passes + +### Phase 1.5: Fastly Runtime Implementation ⏱️ 2-3 hours +- [ ] Create [`tests/environments/mod.rs`](crates/integration-tests/tests/environments/mod.rs) with `RuntimeEnvironment` trait +- [ ] Create [`tests/environments/fastly.rs`](crates/integration-tests/tests/environments/fastly.rs) +- [ ] Implement `FastlyViceroy` struct with `RuntimeEnvironment` trait +- [ ] Implement `ViceroyHandle` for process lifecycle (spawn, kill, wait) +- [ ] Implement dynamic port allocation with `find_available_port()` +- [ ] Implement health check with retry logic (30 attempts × 100ms) +- [ ] Create `RUNTIME_ENVIRONMENTS` registry with Fastly factory +- [ ] Verify: `cargo test -p integration-tests --lib` compiles + +### Phase 2: Framework Abstraction ⏱️ 2-3 hours +- [ ] Create [`tests/frameworks/mod.rs`](crates/integration-tests/tests/frameworks/mod.rs) with `FrontendFramework` trait +- [ ] Create [`tests/frameworks/scenarios.rs`](crates/integration-tests/tests/frameworks/scenarios.rs) +- [ ] Define `TestScenario` enum (HtmlInjection, ScriptServing, AttributeRewriting, GdprSignal) +- [ ] Define `CustomScenario` enum (framework-specific scenarios) +- [ ] Implement scenario runners with error context +- [ ] Create `FRAMEWORKS` registry (empty initially) +- [ ] Verify: Trait compiles, registry pattern works + +### Phase 3: WordPress Implementation ⏱️ 5-7 hours +- [ ] Create [`fixtures/frameworks/wordpress/`](crates/integration-tests/fixtures/frameworks/wordpress/) directory +- [ ] Create [`fixtures/frameworks/wordpress/Dockerfile`](crates/integration-tests/fixtures/frameworks/wordpress/Dockerfile) with SQLite plugin +- [ ] Create minimal WordPress test theme in [`fixtures/frameworks/wordpress/theme/`](crates/integration-tests/fixtures/frameworks/wordpress/theme/) +- [ ] Build WordPress Docker image: `docker build -t test-wordpress:latest fixtures/frameworks/wordpress/` +- [ ] Create [`tests/frameworks/wordpress.rs`](crates/integration-tests/tests/frameworks/wordpress.rs) +- [ ] Implement `WordPress` struct with `FrontendFramework` trait +- [ ] Add WordPress to `FRAMEWORKS` registry +- [ ] Create [`tests/integration.rs`](crates/integration-tests/tests/integration.rs) with `test_combination()` helper +- [ ] Create `test_wordpress_fastly()` test +- [ ] Verify: `cargo test -p integration-tests -- test_wordpress_fastly` passes + +### Phase 4: Next.js Implementation ⏱️ 3-4 hours +- [ ] Create [`fixtures/frameworks/nextjs/`](crates/integration-tests/fixtures/frameworks/nextjs/) directory +- [ ] Create [`fixtures/frameworks/nextjs/package.json`](crates/integration-tests/fixtures/frameworks/nextjs/package.json) +- [ ] Create [`fixtures/frameworks/nextjs/next.config.mjs`](crates/integration-tests/fixtures/frameworks/nextjs/next.config.mjs) +- [ ] Create [`fixtures/frameworks/nextjs/app/page.tsx`](crates/integration-tests/fixtures/frameworks/nextjs/app/page.tsx) with test content +- [ ] Create [`fixtures/frameworks/nextjs/Dockerfile`](crates/integration-tests/fixtures/frameworks/nextjs/Dockerfile) +- [ ] Build Next.js Docker image: `docker build -t test-nextjs:latest fixtures/frameworks/nextjs/` +- [ ] Create [`tests/frameworks/nextjs.rs`](crates/integration-tests/tests/frameworks/nextjs.rs) +- [ ] Implement `NextJs` struct with RSC-specific scenarios +- [ ] Add Next.js to `FRAMEWORKS` registry +- [ ] Create `test_nextjs_fastly()` test +- [ ] Create `test_all_frameworks()` test (iterates `FRAMEWORKS` registry) +- [ ] Verify: `cargo test -p integration-tests` passes for all tests + +### Phase 5: Documentation and CI ⏱️ 3-4 hours +- [ ] Create comprehensive [`crates/integration-tests/README.md`](crates/integration-tests/README.md) + - [ ] Prerequisites section (Docker, Rust, Viceroy) + - [ ] Running tests locally guide + - [ ] How to add a new framework guide + - [ ] How to add a new runtime guide (for future) + - [ ] Troubleshooting section +- [ ] Create [`.github/workflows/integration-tests.yml`](.github/workflows/integration-tests.yml) + - [ ] Build WASM binary step + - [ ] Build Docker images (WordPress, Next.js) + - [ ] Install Viceroy + - [ ] Run integration tests with WASM_BINARY_PATH env var +- [ ] Create [`crates/integration-tests/.dockerignore`](crates/integration-tests/.dockerignore) +- [ ] Update root [`Cargo.toml`](Cargo.toml) workspace members +- [ ] Test CI locally with `act` or similar +- [ ] Verify: CI passes on test branch + +### Success Criteria +- [ ] WordPress tests pass (4 standard + 1 custom scenario) +- [ ] Next.js tests pass (4 standard + 2 custom scenarios) +- [ ] CI completes in < 5 minutes +- [ ] Containers cleanup automatically (no orphans) +- [ ] Tests run in parallel (dynamic port allocation works) +- [ ] Zero flaky tests (20 consecutive runs pass) +- [ ] README instructions work for new contributor +- [ ] Adding a new framework takes < 2 hours (documented process) + +### Post-Implementation Verification +- [ ] Run full test suite: `cargo test --workspace` +- [ ] Run integration tests only: `cargo test -p integration-tests` +- [ ] Run specific framework: `cargo test -p integration-tests -- wordpress` +- [ ] Verify no Docker orphans: `docker ps -a | grep test-` +- [ ] Test parallel execution: `cargo test -p integration-tests` (no `--test-threads=1`) +- [ ] CI passes on PR branch + +## Architecture + +### High-Level Pattern (Multi-Platform) + +``` +┌──────────────────────────────────────────────────────────┐ +│ Matrix Test Runner │ +│ ├─ Discovers registered runtimes (RUNTIME_ENVIRONMENTS) │ +│ ├─ Discovers registered frameworks (FRAMEWORKS) │ +│ ├─ For each runtime: │ +│ │ └─ For each framework: │ +│ │ ├─ Build container from fixture │ +│ │ ├─ Generate platform-specific config │ +│ │ ├─ Spawn runtime process (Viceroy/Wrangler/etc)│ +│ │ ├─ Run standard test scenarios │ +│ │ └─ Run framework-specific scenarios │ +│ └─ Cleanup (automatic via Drop) │ +└──────────────────────────────────────────────────────────┘ +``` + +**Initial Implementation** (1 runtime × 2 frameworks = 2 test combinations): +``` +✓ WordPress + Fastly +✓ Next.js + Fastly +``` + +**Future Expansion** (adding Cloudflare = 2 runtimes × 2 frameworks = 4 combinations): +``` +✓ WordPress + Fastly +✓ WordPress + Cloudflare (future) +✓ Next.js + Fastly +✓ Next.js + Cloudflare (future) +``` + +### Dual Abstraction Design + +#### Frontend Framework Abstraction + +```rust +/// Trait defining how to test a frontend framework +pub trait FrontendFramework { + /// Framework identifier (e.g., "wordpress", "nextjs") + fn id(&self) -> &'static str; + + /// Build Docker container image for this framework + fn build_container(&self) -> Result; + + /// Port the framework serves on inside container + fn container_port(&self) -> u16; + + /// HTTP path to use for health checks + fn health_check_path(&self) -> &str { "/" } + + /// Standard test scenarios applicable to this framework + fn standard_scenarios(&self) -> Vec { + vec![ + TestScenario::HtmlInjection, + TestScenario::ScriptServing, + TestScenario::AttributeRewriting, + TestScenario::GdprSignal, + ] + } + + /// Framework-specific test scenarios (optional) + fn custom_scenarios(&self) -> Vec { + vec![] + } + + /// Additional assertions for this framework (optional) + fn custom_assertions(&self, html: &str) -> Result<()> { + Ok(()) + } +} +``` + +#### Runtime Environment Abstraction + +```rust +/// Trait defining how to run the trusted-server on different platforms +pub trait RuntimeEnvironment: Send + Sync { + /// Platform identifier (e.g., "fastly", "cloudflare") + fn id(&self) -> &'static str; + + /// Spawn runtime with platform-specific configuration + fn spawn(&self, config: &RuntimeConfig) -> Result; + + /// Platform-specific configuration template + fn config_template(&self) -> &str; + + /// Health check endpoint (may differ by platform) + fn health_check_path(&self) -> &str { "/health" } + + /// Platform-specific environment variables + fn env_vars(&self) -> HashMap { HashMap::new() } +} + +/// Platform-agnostic process handle +pub struct RuntimeProcess { + inner: Box, + pub base_url: String, +} + +trait RuntimeProcessHandle { + fn kill(&mut self) -> Result<(), TestError>; + fn wait(&mut self) -> Result<(), TestError>; +} +``` + +### Request Flow (Multi-Platform) + +``` +┌──────────────┐ +│ Test Client │ +│ (reqwest) │ +└──────┬───────┘ + │ GET http://127.0.0.1:/ + ▼ +┌───────────────────────────────────┐ +│ Runtime Process (child process) │ +│ • Fastly: Viceroy │ +│ • Cloudflare: Wrangler │ +│ • (extensible via trait) │ +│ │ +│ Runs WASM binary │ +│ Config: platform-specific toml │ +└───────────────┬───────────────────┘ + │ Proxy to origin_url + ▼ +┌────────────────────────────────────┐ +│ TestContainer (Docker) │ +│ • WordPress │ +│ • Next.js │ +│ • (extensible via trait) │ +│ │ +│ Port: 127.0.0.1:RANDOM │ +└────────────────────────────────────┘ +``` + +**Key Design Decisions:** + +1. **Dual Trait Abstraction**: Separate traits for frontend frameworks and runtime environments - enables independent extensibility +2. **Framework Registry**: Static `FRAMEWORKS` array holds all registered frontend implementations +3. **Runtime Registry**: Static `RUNTIME_ENVIRONMENTS` array holds all registered platform implementations +4. **Matrix Test Runner**: Single test file iterates over both registries, runs all combinations +5. **Fixture Isolation**: Each framework has dedicated `fixtures/frameworks//` directory +6. **Config Isolation**: Each platform has dedicated `fixtures/configs/-template.toml` +7. **Dynamic Port Allocation**: Each runtime gets random available port - enables parallel execution +8. **Platform-Specific Config**: Template-based config generation with platform-specific formats +9. **Automatic Cleanup**: Runtime processes and containers use `Drop` impl for cleanup + +## Directory Structure + +``` +crates/integration-tests/ +├── Cargo.toml # Test crate with testcontainers, reqwest +├── README.md # Setup and how to add frameworks/runtimes +├── fixtures/ +│ ├── configs/ # Platform-specific config templates +│ │ └── fastly-template.toml # Fastly Compute configuration +│ │ # Future: cloudflare-template.toml, spin-template.toml, etc. +│ └── frameworks/ # Frontend framework fixtures +│ ├── wordpress/ +│ │ ├── Dockerfile # WordPress + SQLite custom image +│ │ └── theme/ # Minimal test theme +│ └── nextjs/ +│ ├── Dockerfile # Next.js 14 test app +│ ├── package.json +│ ├── next.config.mjs +│ └── app/ +│ └── page.tsx # Test page with ad slots +├── tests/ +│ ├── common/ +│ │ ├── mod.rs # Re-exports +│ │ ├── runtime.rs # RuntimeEnvironment trait (NEW) +│ │ ├── config.rs # Platform-agnostic config generation +│ │ └── assertions.rs # Shared assertion helpers +│ ├── environments/ # Runtime implementations +│ │ ├── mod.rs # RuntimeEnvironment trait + registry +│ │ └── fastly.rs # FastlyViceroy implementation +│ │ # Future: cloudflare.rs, spin.rs, etc. +│ ├── frameworks/ # Frontend implementations +│ │ ├── mod.rs # FrontendFramework trait + registry +│ │ ├── wordpress.rs # WordPress implementation +│ │ ├── nextjs.rs # Next.js implementation +│ │ └── scenarios.rs # TestScenario enum + runners +│ └── integration.rs # Matrix test runner (frameworks × runtimes) +└── .dockerignore +``` + +## Registry Pattern (Dual Abstraction) + +### Runtime Environment Registry (`tests/environments/mod.rs`) + +```rust +mod fastly; +// Future platforms: +// mod cloudflare; +// mod spin; + +use std::collections::HashMap; +use error_stack::Result; + +/// Trait that all runtime environments must implement +pub trait RuntimeEnvironment: Send + Sync { + fn id(&self) -> &'static str; + fn spawn(&self, config: &RuntimeConfig) -> Result; + fn config_template(&self) -> &str; + fn health_check_path(&self) -> &str { "/health" } + fn env_vars(&self) -> HashMap { HashMap::new() } +} + +/// Platform-agnostic process handle +pub struct RuntimeProcess { + inner: Box, + pub base_url: String, +} + +trait RuntimeProcessHandle { + fn kill(&mut self) -> Result<(), TestError>; + fn wait(&mut self) -> Result<(), TestError>; +} + +/// Runtime factory function type +type RuntimeFactory = fn() -> Box; + +/// Registry of all supported runtime environments +/// Uses function pointers to avoid trait object static initialization issues +pub static RUNTIME_ENVIRONMENTS: &[RuntimeFactory] = &[ + || Box::new(fastly::FastlyViceroy), + // Future: Add Cloudflare, Fermyon Spin, etc. + // || Box::new(cloudflare::CloudflareWrangler), + // || Box::new(spin::FermyonSpin), + + // To add new runtime: + // 1. Create tests/environments/.rs + // 2. Implement RuntimeEnvironment trait + // 3. Add factory here: || Box::new(::) +]; +``` + +### Frontend Framework Registry (`tests/frameworks/mod.rs`) + +```rust +mod wordpress; +mod nextjs; +// To add new framework: create module and register below + +pub mod scenarios; + +use testcontainers::GenericImage; +use error_stack::Result; + +/// Trait that all frontend frameworks must implement +pub trait FrontendFramework: Send + Sync { + fn id(&self) -> &'static str; + fn build_container(&self) -> Result; + fn container_port(&self) -> u16; + fn health_check_path(&self) -> &str { "/" } + fn standard_scenarios(&self) -> Vec; + fn custom_scenarios(&self) -> Vec { vec![] } + fn custom_assertions(&self, html: &str) -> Result<(), TestError> { Ok(()) } +} + +/// Framework factory function type +type FrameworkFactory = fn() -> Box; + +/// Registry of all supported frameworks +/// Uses function pointers to avoid trait object static initialization issues +pub static FRAMEWORKS: &[FrameworkFactory] = &[ + || Box::new(wordpress::WordPress), + || Box::new(nextjs::NextJs), + // To add new framework: + // 1. Create fixtures/frameworks// + // 2. Create tests/frameworks/.rs + // 3. Implement FrontendFramework trait + // 4. Add factory here: || Box::new(::) +]; + +### Test Error Types + +```rust +/// Test error types (platform-agnostic) +#[derive(Debug, derive_more::Display)] +pub enum TestError { + // Runtime environment errors + #[display("Failed to spawn runtime process")] + RuntimeSpawn, + + #[display("Runtime not ready after timeout")] + RuntimeNotReady, + + #[display("Failed to kill runtime process")] + RuntimeKill, + + #[display("Failed to wait for runtime process")] + RuntimeWait, + + // Container errors + #[display("Container failed to start: {reason}")] + ContainerStart { reason: String }, + + #[display("Container operation timed out")] + ContainerTimeout, + + // HTTP errors + #[display("HTTP request failed")] + HttpRequest, + + #[display("Failed to parse response")] + ResponseParse, + + // Assertion errors + #[display("Script tag not found in HTML")] + ScriptTagNotFound, + + #[display("Missing module: {module}")] + MissingModule { module: String }, + + #[display("Invalid CSS selector")] + InvalidSelector, + + #[display("Element not found")] + ElementNotFound, + + #[display("Attribute not rewritten")] + AttributeNotRewritten, + + #[display("GDPR signal missing from response")] + GdprSignalMissing, + + // Configuration errors + #[display("Config parse error")] + ConfigParse, + + #[display("Config write error")] + ConfigWrite, + + #[display("Config serialization error")] + ConfigSerialize, + + // Resource errors + #[display("WASM binary not found")] + WasmBinaryNotFound, + + #[display("No available port found")] + NoPortAvailable, +} + +impl core::error::Error for TestError {} +``` + +### Runtime Implementation Examples + +#### Fastly Runtime (`tests/environments/fastly.rs`) + +```rust +use super::*; +use std::process::{Child, Command}; + +pub struct FastlyViceroy; + +impl RuntimeEnvironment for FastlyViceroy { + fn id(&self) -> &'static str { + "fastly" + } + + fn spawn(&self, config: &RuntimeConfig) -> Result { + let port = find_available_port()?; + + let mut child = Command::new("viceroy") + .arg("run") + .arg("-C") + .arg(&config.config_path) + .arg("--addr") + .arg(format!("127.0.0.1:{}", port)) + .arg("--") + .arg(&config.wasm_path) + .spawn() + .change_context(TestError::RuntimeSpawn)?; + + let base_url = format!("http://127.0.0.1:{}", port); + wait_for_ready(&base_url, self.health_check_path())?; + + Ok(RuntimeProcess { + inner: Box::new(ViceroyHandle { child }), + base_url, + }) + } + + fn config_template(&self) -> &str { + include_str!("../../fixtures/configs/fastly-template.toml") + } +} + +struct ViceroyHandle { + child: Child, +} + +impl RuntimeProcessHandle for ViceroyHandle { + fn kill(&mut self) -> Result<(), TestError> { + self.child.kill() + .change_context(TestError::RuntimeKill)?; + Ok(()) + } + + fn wait(&mut self) -> Result<(), TestError> { + self.child.wait() + .change_context(TestError::RuntimeWait)?; + Ok(()) + } +} +``` + +**Note**: Cloudflare runtime implementation example moved to "Future Enhancements" section. + +To add a new runtime platform, follow the same pattern as Fastly above: +1. Create `tests/environments/.rs` +2. Implement `RuntimeEnvironment` trait +3. Create platform-specific process handle +4. Register in `RUNTIME_ENVIRONMENTS` array + +### Frontend Framework Implementation Examples + +#### WordPress Implementation (`tests/frameworks/wordpress.rs`) + +```rust +use super::{FrontendFramework, scenarios::*}; +use testcontainers::{GenericImage, WaitFor}; + +pub struct WordPress; + +impl FrontendFramework for WordPress { + fn id(&self) -> &'static str { + "wordpress" + } + + fn build_container(&self) -> Result { + // Use wordpress:cli with minimal setup (no MySQL required for basic HTML serving) + // Alternative: Use pre-configured image with SQLite plugin + Ok(GenericImage::new("wordpress", "cli") + .with_exposed_port(8080) + .with_env_var("WORDPRESS_DEBUG", "0") + .with_wait_for(WaitFor::message_on_stdout("WordPress Ready"))) + } + + fn container_port(&self) -> u16 { + 8080 + } + + fn standard_scenarios(&self) -> Vec { + vec![ + TestScenario::HtmlInjection, + TestScenario::ScriptServing { modules: vec!["core", "prebid"] }, + TestScenario::AttributeRewriting, + TestScenario::GdprSignal, + ] + } + + fn custom_scenarios(&self) -> Vec { + vec![ + CustomScenario::WordPressAdminInjection, + ] + } + + fn custom_assertions(&self, html: &str) -> Result<(), TestError> { + // Verify WordPress-specific HTML structure preserved + ensure!(html.contains("wp-content"), "should preserve wp-content paths"); + Ok(()) + } +} +``` + +**Note**: WordPress requires additional setup for full functionality. For Phase 3, consider: +- **Option A** (Recommended): Use `wordpress:cli` image or pre-built image with SQLite plugin - simpler, faster startup +- **Option B**: Multi-container setup with MySQL - more realistic but adds complexity + +### Building Container Images + +For frameworks that require custom Docker images (like Next.js), the images must be built before tests run: + +**Option 1: Pre-build in CI** (Recommended) +```yaml +# In .github/workflows/test.yml +- name: Build test container images + run: | + docker build -t test-nextjs:latest \ + crates/integration-tests/fixtures/frameworks/nextjs/ +``` + +**Option 2: Build on-demand in tests** +```rust +// In tests/frameworks/nextjs.rs +fn build_container(&self) -> Result { + // Check if image exists, build if not + let image_name = "test-nextjs:latest"; + + // Use testcontainers ImageBuild API (if available) + // or shell out to docker build command + Ok(GenericImage::new(image_name, "latest") + .with_exposed_port(3000)) +} +``` + +**Option 3: Use docker-compose** (Future enhancement) +```rust +// Multi-service orchestration for complex setups +``` + +### Next.js Implementation (`tests/frameworks/nextjs.rs`) + +```rust +use super::{FrontendFramework, scenarios::*}; +use testcontainers::GenericImage; + +pub struct NextJs; + +impl FrontendFramework for NextJs { + fn id(&self) -> &'static str { + "nextjs" + } + + fn build_container(&self) -> Result { + // Build from Dockerfile in fixtures/frameworks/nextjs/ + Ok(GenericImage::new("test-nextjs", "latest") + .with_exposed_port(3000) + .with_wait_for(WaitFor::message_on_stdout("ready"))) + } + + fn container_port(&self) -> u16 { + 3000 + } + + fn standard_scenarios(&self) -> Vec { + vec![ + TestScenario::HtmlInjection, + TestScenario::ScriptServing { modules: vec!["core", "prebid", "lockr"] }, + TestScenario::AttributeRewriting, + TestScenario::GdprSignal, + ] + } + + fn custom_scenarios(&self) -> Vec { + vec![ + CustomScenario::NextJsRscFlight, // RSC streaming format + CustomScenario::NextJsServerActions, + ] + } + + fn custom_assertions(&self, html: &str) -> Result<(), TestError> { + // Verify Next.js hydration markers preserved + ensure!(html.contains("__NEXT_DATA__"), "should preserve Next.js data"); + Ok(()) + } +} +``` + +### Test Scenarios (`tests/frameworks/scenarios.rs`) + +```rust +/// Standard test scenarios applicable to all frameworks +#[derive(Debug, Clone)] +pub enum TestScenario { + /// Verify + + + + "#; + + assert_script_tag_present(html, &["core", "prebid"]) + .expect("should find script tag with expected modules"); + } + + #[test] + fn test_assert_script_tag_present_missing_module() { + let html = r#" + + + + + + + + "#; + + let result = assert_script_tag_present(html, &["core", "lockr"]); + assert!( + result.is_err(), + "should fail when expected module is missing" + ); + } + + #[test] + fn test_assert_attribute_rewritten_success() { + let html = r#" + Link + "#; + + assert_attribute_rewritten(html, "a[href]", "href", "/first-party/proxy?url=") + .expect("should find rewritten attribute"); + } + + #[test] + fn test_assert_attribute_rewritten_not_rewritten() { + let html = r#" + Link + "#; + + let result = assert_attribute_rewritten(html, "a[href]", "href", "/first-party/proxy?url="); + assert!(result.is_err(), "should fail when attribute not rewritten"); + } + + #[test] + fn test_assert_element_count_success() { + let html = r#" +
+

First

+

Second

+

Third

+
+ "#; + + assert_element_count(html, "p", 3).expect("should count elements correctly"); + } + + #[test] + fn test_assert_element_count_mismatch() { + let html = r#" +
+

First

+

Second

+
+ "#; + + let result = assert_element_count(html, "p", 3); + assert!(result.is_err(), "should fail when count doesn't match"); + } +} diff --git a/crates/integration-tests/tests/common/config.rs b/crates/integration-tests/tests/common/config.rs new file mode 100644 index 00000000..6bf21177 --- /dev/null +++ b/crates/integration-tests/tests/common/config.rs @@ -0,0 +1,91 @@ +use super::runtime::TestError; +use error_stack::{Result, ResultExt}; +use std::io::Write; +use std::path::PathBuf; +use tempfile::NamedTempFile; + +/// Builder for runtime configuration +pub struct RuntimeConfigBuilder { + template: String, + origin_url: Option, + integrations: Vec, + wasm_path: Option, +} + +impl RuntimeConfigBuilder { + pub fn new(template: &str) -> Self { + Self { + template: template.to_string(), + origin_url: None, + integrations: Vec::new(), + wasm_path: None, + } + } + + pub fn with_origin_url(mut self, url: String) -> Self { + self.origin_url = Some(url); + self + } + + pub fn with_integrations(mut self, ids: Vec<&str>) -> Self { + self.integrations = ids.iter().map(|s| s.to_string()).collect(); + self + } + + pub fn with_wasm_path(mut self, path: PathBuf) -> Self { + self.wasm_path = Some(path); + self + } + + pub fn build(self) -> Result { + // Parse template as TOML + let mut config: toml::Value = toml::from_str(&self.template) + .change_context(TestError::ConfigParse)?; + + // Set origin_url if provided + if let Some(origin_url) = self.origin_url { + if let Some(publisher) = config.get_mut("publisher") { + if let Some(publisher_table) = publisher.as_table_mut() { + publisher_table.insert( + "origin_url".to_string(), + toml::Value::String(origin_url), + ); + } + } + } + + // Enable integrations if provided + if !self.integrations.is_empty() { + if let Some(integrations_config) = config + .get_mut("integrations") + .and_then(|v| v.get_mut("config")) + .and_then(|v| v.as_table_mut()) + { + for integration_id in &self.integrations { + if let Some(integration_entry) = integrations_config.get_mut(integration_id) { + if let Some(table) = integration_entry.as_table_mut() { + table.insert("enabled".to_string(), toml::Value::Boolean(true)); + } + } + } + } + } + + // Write to temp file + let mut file = NamedTempFile::new().change_context(TestError::ConfigWrite)?; + let content = + toml::to_string_pretty(&config).change_context(TestError::ConfigSerialize)?; + file.write_all(content.as_bytes()) + .change_context(TestError::ConfigWrite)?; + + let config_path = file.into_temp_path().to_path_buf(); + let wasm_path = self + .wasm_path + .unwrap_or_else(super::runtime::wasm_binary_path); + + Ok(super::RuntimeConfig { + config_path, + wasm_path, + }) + } +} diff --git a/crates/integration-tests/tests/common/mod.rs b/crates/integration-tests/tests/common/mod.rs new file mode 100644 index 00000000..af63a198 --- /dev/null +++ b/crates/integration-tests/tests/common/mod.rs @@ -0,0 +1,7 @@ +pub mod assertions; +pub mod config; +pub mod runtime; + +pub use assertions::*; +pub use config::*; +pub use runtime::*; diff --git a/crates/integration-tests/tests/common/runtime.rs b/crates/integration-tests/tests/common/runtime.rs new file mode 100644 index 00000000..8321081b --- /dev/null +++ b/crates/integration-tests/tests/common/runtime.rs @@ -0,0 +1,122 @@ +use error_stack::{Result, ResultExt}; +use std::collections::HashMap; +use std::path::PathBuf; + +/// Test error types (platform-agnostic) +#[derive(Debug, derive_more::Display)] +pub enum TestError { + // Runtime environment errors + #[display("Failed to spawn runtime process")] + RuntimeSpawn, + + #[display("Runtime not ready after timeout")] + RuntimeNotReady, + + #[display("Failed to kill runtime process")] + RuntimeKill, + + #[display("Failed to wait for runtime process")] + RuntimeWait, + + // Container errors + #[display("Container failed to start: {reason}")] + ContainerStart { reason: String }, + + #[display("Container operation timed out")] + ContainerTimeout, + + // HTTP errors + #[display("HTTP request failed")] + HttpRequest, + + #[display("Failed to parse response")] + ResponseParse, + + // Assertion errors + #[display("Script tag not found in HTML")] + ScriptTagNotFound, + + #[display("Missing module: {module}")] + MissingModule { module: String }, + + #[display("Invalid CSS selector")] + InvalidSelector, + + #[display("Element not found")] + ElementNotFound, + + #[display("Attribute not rewritten")] + AttributeNotRewritten, + + #[display("GDPR signal missing from response")] + GdprSignalMissing, + + // Configuration errors + #[display("Config parse error")] + ConfigParse, + + #[display("Config write error")] + ConfigWrite, + + #[display("Config serialization error")] + ConfigSerialize, + + // Resource errors + #[display("WASM binary not found")] + WasmBinaryNotFound, + + #[display("No available port found")] + NoPortAvailable, +} + +impl core::error::Error for TestError {} + +/// Configuration for runtime environments +pub struct RuntimeConfig { + pub config_path: PathBuf, + pub wasm_path: PathBuf, +} + +/// Platform-agnostic process handle +pub struct RuntimeProcess { + pub inner: Box, + pub base_url: String, +} + +/// Trait for runtime process lifecycle management +pub trait RuntimeProcessHandle: Send + Sync { + fn kill(&mut self) -> Result<(), TestError>; + fn wait(&mut self) -> Result<(), TestError>; +} + +/// Trait defining how to run the trusted-server on different platforms +pub trait RuntimeEnvironment: Send + Sync { + /// Platform identifier (e.g., "fastly", "cloudflare") + fn id(&self) -> &'static str; + + /// Spawn runtime with platform-specific configuration + fn spawn(&self, config: &RuntimeConfig) -> Result; + + /// Platform-specific configuration template + fn config_template(&self) -> &str; + + /// Health check endpoint (may differ by platform) + fn health_check_path(&self) -> &str { + "/health" + } + + /// Platform-specific environment variables + fn env_vars(&self) -> HashMap { + HashMap::new() + } +} + +/// Get path to WASM binary, respecting environment variable +pub fn wasm_binary_path() -> PathBuf { + std::env::var("WASM_BINARY_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../target/wasm32-wasip1/release/trusted-server-fastly.wasm") + }) +} From 2d2600ea2714faecdef75a7ebd52dc730a85457b Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Thu, 5 Mar 2026 19:35:05 +0530 Subject: [PATCH 02/15] Feature integration test with TestContainers --- .github/workflows/integration-tests.yml | 68 ++++++ INTEGRATION_TESTS_PLAN.md | 138 +++++------ crates/integration-tests/.dockerignore | 6 + crates/integration-tests/Cargo.toml | 2 +- .../fixtures/configs/fastly-template.toml | 97 ++++++++ .../fixtures/configs/viceroy-template.toml | 38 +++ .../fixtures/frameworks/nextjs/Dockerfile | 37 +++ .../fixtures/frameworks/nextjs/app/layout.tsx | 17 ++ .../fixtures/frameworks/nextjs/app/page.tsx | 20 ++ .../frameworks/nextjs/next.config.mjs | 6 + .../fixtures/frameworks/nextjs/package.json | 15 ++ .../fixtures/frameworks/wordpress/Dockerfile | 22 ++ .../frameworks/wordpress/theme/index.php | 41 ++++ .../wordpress/theme/wp-admin/index.php | 13 + .../tests/common/assertions.rs | 183 +++++++------- .../integration-tests/tests/common/config.rs | 29 +-- .../integration-tests/tests/common/runtime.rs | 34 ++- .../tests/environments/fastly.rs | 103 ++++++++ .../tests/environments/mod.rs | 74 ++++++ .../integration-tests/tests/frameworks/mod.rs | 64 +++++ .../tests/frameworks/nextjs.rs | 52 ++++ .../tests/frameworks/scenarios.rs | 228 ++++++++++++++++++ .../tests/frameworks/wordpress.rs | 50 ++++ crates/integration-tests/tests/integration.rs | 143 +++++++++++ 24 files changed, 1294 insertions(+), 186 deletions(-) create mode 100644 .github/workflows/integration-tests.yml create mode 100644 crates/integration-tests/.dockerignore create mode 100644 crates/integration-tests/fixtures/configs/fastly-template.toml create mode 100644 crates/integration-tests/fixtures/configs/viceroy-template.toml create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/Dockerfile create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/app/layout.tsx create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/app/page.tsx create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/next.config.mjs create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/package.json create mode 100644 crates/integration-tests/fixtures/frameworks/wordpress/Dockerfile create mode 100644 crates/integration-tests/fixtures/frameworks/wordpress/theme/index.php create mode 100644 crates/integration-tests/fixtures/frameworks/wordpress/theme/wp-admin/index.php create mode 100644 crates/integration-tests/tests/environments/fastly.rs create mode 100644 crates/integration-tests/tests/environments/mod.rs create mode 100644 crates/integration-tests/tests/frameworks/mod.rs create mode 100644 crates/integration-tests/tests/frameworks/nextjs.rs create mode 100644 crates/integration-tests/tests/frameworks/scenarios.rs create mode 100644 crates/integration-tests/tests/frameworks/wordpress.rs create mode 100644 crates/integration-tests/tests/integration.rs diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 00000000..67ec68af --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,68 @@ +name: "Integration Tests" + +permissions: + contents: read + +on: + push: + branches: [main] + pull_request: + branches: [main] + paths: + - "crates/integration-tests/**" + - "crates/common/**" + - "crates/fastly/**" + - ".github/workflows/integration-tests.yml" + +jobs: + integration-tests: + name: integration tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Retrieve Rust version + id: rust-version + run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + shell: bash + + - name: Set up Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ steps.rust-version.outputs.rust-version }} + target: wasm32-wasip1 + cache-shared-key: cargo-${{ runner.os }} + + - name: Get Viceroy cache key + id: viceroy-rev + run: echo "sha=$(git ls-remote https://github.com/fastly/Viceroy HEAD | cut -f1)" >> $GITHUB_OUTPUT + + - name: Cache Viceroy binary + id: cache-viceroy + uses: actions/cache@v4 + with: + path: ~/.cargo/bin/viceroy + key: viceroy-${{ runner.os }}-${{ steps.viceroy-rev.outputs.sha }} + + - name: Install Viceroy + if: steps.cache-viceroy.outputs.cache-hit != 'true' + run: cargo install --git https://github.com/fastly/Viceroy viceroy + + - name: Build WASM binary + run: cargo build --bin trusted-server-fastly --release --target wasm32-wasip1 + + - name: Build WordPress test container + run: | + docker build -t test-wordpress:latest \ + crates/integration-tests/fixtures/frameworks/wordpress/ + + - name: Build Next.js test container + run: | + docker build -t test-nextjs:latest \ + crates/integration-tests/fixtures/frameworks/nextjs/ + + - name: Run integration tests + run: cargo test -p integration-tests --target x86_64-unknown-linux-gnu + env: + WASM_BINARY_PATH: target/wasm32-wasip1/release/trusted-server-fastly.wasm + RUST_LOG: info diff --git a/INTEGRATION_TESTS_PLAN.md b/INTEGRATION_TESTS_PLAN.md index b7153c33..4a98fa41 100644 --- a/INTEGRATION_TESTS_PLAN.md +++ b/INTEGRATION_TESTS_PLAN.md @@ -76,77 +76,73 @@ Track progress for each phase using this checklist: - [x] Start Viceroy manually to test health endpoint - [x] Verify `curl http://127.0.0.1:7676/health` returns 200 OK -### Phase 1: Core Infrastructure ⏱️ 4-6 hours -- [ ] Add `integration-tests` to workspace members in root `Cargo.toml` -- [ ] Create [`crates/integration-tests/Cargo.toml`](crates/integration-tests/Cargo.toml) with dependencies -- [ ] Create [`tests/common/mod.rs`](crates/integration-tests/tests/common/mod.rs) (re-exports) -- [ ] Define `RuntimeEnvironment` trait in [`tests/common/runtime.rs`](crates/integration-tests/tests/common/runtime.rs) -- [ ] Implement `RuntimeConfig` struct with platform-agnostic interface -- [ ] Create [`tests/common/config.rs`](crates/integration-tests/tests/common/config.rs) for config generation -- [ ] Implement assertion helpers in [`tests/common/assertions.rs`](crates/integration-tests/tests/common/assertions.rs) -- [ ] Create [`fixtures/configs/fastly-template.toml`](crates/integration-tests/fixtures/configs/fastly-template.toml) -- [ ] Verify: `cargo check -p integration-tests` passes - -### Phase 1.5: Fastly Runtime Implementation ⏱️ 2-3 hours -- [ ] Create [`tests/environments/mod.rs`](crates/integration-tests/tests/environments/mod.rs) with `RuntimeEnvironment` trait -- [ ] Create [`tests/environments/fastly.rs`](crates/integration-tests/tests/environments/fastly.rs) -- [ ] Implement `FastlyViceroy` struct with `RuntimeEnvironment` trait -- [ ] Implement `ViceroyHandle` for process lifecycle (spawn, kill, wait) -- [ ] Implement dynamic port allocation with `find_available_port()` -- [ ] Implement health check with retry logic (30 attempts × 100ms) -- [ ] Create `RUNTIME_ENVIRONMENTS` registry with Fastly factory -- [ ] Verify: `cargo test -p integration-tests --lib` compiles - -### Phase 2: Framework Abstraction ⏱️ 2-3 hours -- [ ] Create [`tests/frameworks/mod.rs`](crates/integration-tests/tests/frameworks/mod.rs) with `FrontendFramework` trait -- [ ] Create [`tests/frameworks/scenarios.rs`](crates/integration-tests/tests/frameworks/scenarios.rs) -- [ ] Define `TestScenario` enum (HtmlInjection, ScriptServing, AttributeRewriting, GdprSignal) -- [ ] Define `CustomScenario` enum (framework-specific scenarios) -- [ ] Implement scenario runners with error context -- [ ] Create `FRAMEWORKS` registry (empty initially) -- [ ] Verify: Trait compiles, registry pattern works - -### Phase 3: WordPress Implementation ⏱️ 5-7 hours -- [ ] Create [`fixtures/frameworks/wordpress/`](crates/integration-tests/fixtures/frameworks/wordpress/) directory -- [ ] Create [`fixtures/frameworks/wordpress/Dockerfile`](crates/integration-tests/fixtures/frameworks/wordpress/Dockerfile) with SQLite plugin -- [ ] Create minimal WordPress test theme in [`fixtures/frameworks/wordpress/theme/`](crates/integration-tests/fixtures/frameworks/wordpress/theme/) -- [ ] Build WordPress Docker image: `docker build -t test-wordpress:latest fixtures/frameworks/wordpress/` -- [ ] Create [`tests/frameworks/wordpress.rs`](crates/integration-tests/tests/frameworks/wordpress.rs) -- [ ] Implement `WordPress` struct with `FrontendFramework` trait -- [ ] Add WordPress to `FRAMEWORKS` registry -- [ ] Create [`tests/integration.rs`](crates/integration-tests/tests/integration.rs) with `test_combination()` helper -- [ ] Create `test_wordpress_fastly()` test -- [ ] Verify: `cargo test -p integration-tests -- test_wordpress_fastly` passes - -### Phase 4: Next.js Implementation ⏱️ 3-4 hours -- [ ] Create [`fixtures/frameworks/nextjs/`](crates/integration-tests/fixtures/frameworks/nextjs/) directory -- [ ] Create [`fixtures/frameworks/nextjs/package.json`](crates/integration-tests/fixtures/frameworks/nextjs/package.json) -- [ ] Create [`fixtures/frameworks/nextjs/next.config.mjs`](crates/integration-tests/fixtures/frameworks/nextjs/next.config.mjs) -- [ ] Create [`fixtures/frameworks/nextjs/app/page.tsx`](crates/integration-tests/fixtures/frameworks/nextjs/app/page.tsx) with test content -- [ ] Create [`fixtures/frameworks/nextjs/Dockerfile`](crates/integration-tests/fixtures/frameworks/nextjs/Dockerfile) -- [ ] Build Next.js Docker image: `docker build -t test-nextjs:latest fixtures/frameworks/nextjs/` -- [ ] Create [`tests/frameworks/nextjs.rs`](crates/integration-tests/tests/frameworks/nextjs.rs) -- [ ] Implement `NextJs` struct with RSC-specific scenarios -- [ ] Add Next.js to `FRAMEWORKS` registry -- [ ] Create `test_nextjs_fastly()` test -- [ ] Create `test_all_frameworks()` test (iterates `FRAMEWORKS` registry) -- [ ] Verify: `cargo test -p integration-tests` passes for all tests - -### Phase 5: Documentation and CI ⏱️ 3-4 hours -- [ ] Create comprehensive [`crates/integration-tests/README.md`](crates/integration-tests/README.md) - - [ ] Prerequisites section (Docker, Rust, Viceroy) - - [ ] Running tests locally guide - - [ ] How to add a new framework guide - - [ ] How to add a new runtime guide (for future) - - [ ] Troubleshooting section -- [ ] Create [`.github/workflows/integration-tests.yml`](.github/workflows/integration-tests.yml) - - [ ] Build WASM binary step - - [ ] Build Docker images (WordPress, Next.js) - - [ ] Install Viceroy - - [ ] Run integration tests with WASM_BINARY_PATH env var -- [ ] Create [`crates/integration-tests/.dockerignore`](crates/integration-tests/.dockerignore) -- [ ] Update root [`Cargo.toml`](Cargo.toml) workspace members -- [ ] Test CI locally with `act` or similar +### Phase 1: Core Infrastructure ⏱️ 4-6 hours ✅ COMPLETE +- [x] Add `integration-tests` to workspace members in root `Cargo.toml` +- [x] Create [`crates/integration-tests/Cargo.toml`](crates/integration-tests/Cargo.toml) with dependencies +- [x] Create [`tests/common/mod.rs`](crates/integration-tests/tests/common/mod.rs) (re-exports) +- [x] Define `RuntimeEnvironment` trait in [`tests/common/runtime.rs`](crates/integration-tests/tests/common/runtime.rs) +- [x] Implement `RuntimeConfig` struct with platform-agnostic interface +- [x] Create [`tests/common/config.rs`](crates/integration-tests/tests/common/config.rs) for config generation +- [x] Implement assertion helpers in [`tests/common/assertions.rs`](crates/integration-tests/tests/common/assertions.rs) +- [x] Create [`fixtures/configs/fastly-template.toml`](crates/integration-tests/fixtures/configs/fastly-template.toml) +- [x] Create [`fixtures/configs/viceroy-template.toml`](crates/integration-tests/fixtures/configs/viceroy-template.toml) +- [x] Verify: `cargo check -p integration-tests` passes +- [x] Verify: 9/9 assertion unit tests pass + +### Phase 1.5: Fastly Runtime Implementation ⏱️ 2-3 hours ✅ COMPLETE +- [x] Create [`tests/environments/mod.rs`](crates/integration-tests/tests/environments/mod.rs) with `RuntimeEnvironment` trait +- [x] Create [`tests/environments/fastly.rs`](crates/integration-tests/tests/environments/fastly.rs) +- [x] Implement `FastlyViceroy` struct with `RuntimeEnvironment` trait +- [x] Implement `ViceroyHandle` for process lifecycle (spawn, kill, wait, Drop) +- [x] Implement dynamic port allocation with `find_available_port()` +- [x] Implement health check with retry logic (30 attempts × 100ms) +- [x] Create `RUNTIME_ENVIRONMENTS` registry with Fastly factory +- [x] Verify: test binary compiles for native target + +### Phase 2: Framework Abstraction ⏱️ 2-3 hours ✅ COMPLETE +- [x] Create [`tests/frameworks/mod.rs`](crates/integration-tests/tests/frameworks/mod.rs) with `FrontendFramework` trait +- [x] Create [`tests/frameworks/scenarios.rs`](crates/integration-tests/tests/frameworks/scenarios.rs) +- [x] Define `TestScenario` enum (HtmlInjection, ScriptServing, AttributeRewriting, GdprSignal) +- [x] Define `CustomScenario` enum (WordPressAdminInjection, NextJsRscFlight, NextJsServerActions) +- [x] Implement scenario runners with error context +- [x] Create `FRAMEWORKS` registry with WordPress and Next.js factories +- [x] Verify: Trait compiles, registry pattern works + +### Phase 3: WordPress Implementation ⏱️ 5-7 hours ✅ COMPLETE +- [x] Create [`fixtures/frameworks/wordpress/`](crates/integration-tests/fixtures/frameworks/wordpress/) directory +- [x] Create [`fixtures/frameworks/wordpress/Dockerfile`](crates/integration-tests/fixtures/frameworks/wordpress/Dockerfile) (PHP CLI with test theme) +- [x] Create minimal WordPress test theme in [`fixtures/frameworks/wordpress/theme/`](crates/integration-tests/fixtures/frameworks/wordpress/theme/) +- [x] Create wp-admin test page for admin injection scenario +- [x] Create [`tests/frameworks/wordpress.rs`](crates/integration-tests/tests/frameworks/wordpress.rs) +- [x] Implement `WordPress` struct with `FrontendFramework` trait +- [x] Add WordPress to `FRAMEWORKS` registry +- [x] Create [`tests/integration.rs`](crates/integration-tests/tests/integration.rs) with `test_combination()` helper +- [x] Create `test_wordpress_fastly()` test +- [ ] Verify: `cargo test -p integration-tests -- test_wordpress_fastly` passes (requires Docker) + +### Phase 4: Next.js Implementation ⏱️ 3-4 hours ✅ COMPLETE +- [x] Create [`fixtures/frameworks/nextjs/`](crates/integration-tests/fixtures/frameworks/nextjs/) directory +- [x] Create [`fixtures/frameworks/nextjs/package.json`](crates/integration-tests/fixtures/frameworks/nextjs/package.json) +- [x] Create [`fixtures/frameworks/nextjs/next.config.mjs`](crates/integration-tests/fixtures/frameworks/nextjs/next.config.mjs) (standalone output) +- [x] Create [`fixtures/frameworks/nextjs/app/layout.tsx`](crates/integration-tests/fixtures/frameworks/nextjs/app/layout.tsx) +- [x] Create [`fixtures/frameworks/nextjs/app/page.tsx`](crates/integration-tests/fixtures/frameworks/nextjs/app/page.tsx) with ad slot test content +- [x] Create [`fixtures/frameworks/nextjs/Dockerfile`](crates/integration-tests/fixtures/frameworks/nextjs/Dockerfile) (multi-stage build) +- [x] Create [`tests/frameworks/nextjs.rs`](crates/integration-tests/tests/frameworks/nextjs.rs) +- [x] Implement `NextJs` struct with RSC-specific scenarios +- [x] Add Next.js to `FRAMEWORKS` registry +- [x] Create `test_nextjs_fastly()` test +- [x] Create `test_all_combinations()` matrix test +- [ ] Verify: `cargo test -p integration-tests` passes for all tests (requires Docker) + +### Phase 5: Documentation and CI ⏱️ 3-4 hours ✅ COMPLETE +- [x] Create [`.github/workflows/integration-tests.yml`](.github/workflows/integration-tests.yml) + - [x] Build WASM binary step + - [x] Build Docker images (WordPress, Next.js) + - [x] Install Viceroy (with caching) + - [x] Run integration tests with WASM_BINARY_PATH env var +- [x] Create [`crates/integration-tests/.dockerignore`](crates/integration-tests/.dockerignore) +- [x] Update root [`Cargo.toml`](Cargo.toml) workspace members +- [ ] Test CI on PR branch - [ ] Verify: CI passes on test branch ### Success Criteria diff --git a/crates/integration-tests/.dockerignore b/crates/integration-tests/.dockerignore new file mode 100644 index 00000000..1004a143 --- /dev/null +++ b/crates/integration-tests/.dockerignore @@ -0,0 +1,6 @@ +target/ +*.rs +Cargo.toml +Cargo.lock +tests/ +README.md diff --git a/crates/integration-tests/Cargo.toml b/crates/integration-tests/Cargo.toml index 9f65f9e6..224d854c 100644 --- a/crates/integration-tests/Cargo.toml +++ b/crates/integration-tests/Cargo.toml @@ -10,7 +10,7 @@ path = "tests/integration.rs" harness = true [dev-dependencies] -testcontainers = "0.25" +testcontainers = { version = "0.25", features = ["blocking"] } reqwest = { version = "0.12", features = ["blocking"] } scraper = "0.21" tempfile = "3.0" diff --git a/crates/integration-tests/fixtures/configs/fastly-template.toml b/crates/integration-tests/fixtures/configs/fastly-template.toml new file mode 100644 index 00000000..9e377a74 --- /dev/null +++ b/crates/integration-tests/fixtures/configs/fastly-template.toml @@ -0,0 +1,97 @@ +# Fastly Compute configuration template for integration tests. +# The test harness mutates `publisher.origin_url` and integration toggles +# at runtime via RuntimeConfigBuilder. + +[[handlers]] +path = "^/secure" +username = "user" +password = "pass" + +[publisher] +domain = "test-publisher.com" +cookie_domain = ".test-publisher.com" +origin_url = "PLACEHOLDER" +proxy_secret = "integration-test-proxy-secret" + +[synthetic] +counter_store = "counter_store" +opid_store = "opid_store" +secret_key = "integration-test-secret" +template = "{{ client_ip }}:{{ user_agent }}:{{ accept_language }}:{{ accept_encoding }}" + +[request_signing] +enabled = false +config_store_id = "unused" +secret_store_id = "unused" + +[integrations.prebid] +enabled = false +server_url = "http://127.0.0.1:9999" +timeout_ms = 1000 +bidders = ["appnexus"] +debug = false + +[integrations.nextjs] +enabled = false +rewrite_attributes = ["href", "link", "siteBaseUrl", "siteProductionDomain", "url"] +max_combined_payload_bytes = 10485760 + +[integrations.testlight] +endpoint = "https://testlight.example/openrtb2/auction" +timeout_ms = 1200 +rewrite_scripts = true + +[integrations.didomi] +enabled = false +sdk_origin = "https://sdk.privacy-center.org" +api_origin = "https://api.privacy-center.org" + +[integrations.permutive] +enabled = false +organization_id = "" +workspace_id = "" +project_id = "" +api_endpoint = "https://api.permutive.com" +secure_signals_endpoint = "https://secure-signals.permutive.app" + +[integrations.lockr] +enabled = false +app_id = "" +api_endpoint = "https://identity.loc.kr" +sdk_url = "https://aim.loc.kr/identity-lockr-v1.0.js" +cache_ttl_seconds = 3600 +rewrite_sdk = true + +[integrations.datadome] +enabled = false +sdk_origin = "https://js.datadome.co" +api_origin = "https://api-js.datadome.co" +cache_ttl_seconds = 3600 +rewrite_sdk = true + +[integrations.gpt] +enabled = false +script_url = "https://securepubads.g.doubleclick.net/tag/js/gpt.js" +cache_ttl_seconds = 3600 +rewrite_script = true + +[auction] +enabled = false +providers = ["prebid"] +timeout_ms = 2000 +allowed_context_keys = [] + +[integrations.aps] +enabled = false +pub_id = "test-aps-publisher-id" +endpoint = "https://origin-mocktioneer.cdintel.com/e/dtb/bid" +timeout_ms = 1000 + +[integrations.google_tag_manager] +enabled = false +container_id = "GTM-TEST" + +[integrations.adserver_mock] +enabled = false +endpoint = "https://origin-mocktioneer.cdintel.com/adserver/mediate" +timeout_ms = 1000 diff --git a/crates/integration-tests/fixtures/configs/viceroy-template.toml b/crates/integration-tests/fixtures/configs/viceroy-template.toml new file mode 100644 index 00000000..3129c3f5 --- /dev/null +++ b/crates/integration-tests/fixtures/configs/viceroy-template.toml @@ -0,0 +1,38 @@ +# Viceroy local server configuration template for integration tests. +# This configures the Viceroy runtime itself (backends, KV stores, etc.), +# separate from the application config (trusted-server.toml). + +[local_server] + + [local_server.backends] + + [local_server.kv_stores] + [[local_server.kv_stores.counter_store]] + key = "placeholder" + data = "placeholder" + + [[local_server.kv_stores.opid_store]] + key = "placeholder" + data = "placeholder" + + [[local_server.kv_stores.creative_store]] + key = "placeholder" + data = "placeholder" + + [local_server.secret_stores] + [[local_server.secret_stores.signing_keys]] + key = "ts-2025-10-A" + data = "NVnTYrw5xoyTJDOwoUWoPJO3A6UCCXOJJUzgGTxxx7k=" + + [[local_server.secret_stores.api-keys]] + key = "api_key" + data = "test-api-key" + + [local_server.config_stores] + [local_server.config_stores.jwks_store] + format = "inline-toml" + [local_server.config_stores.jwks_store.contents] + ts-2025-10-A = "{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\"kid\":\"ts-2025-10-A\",\"use\":\"sig\",\"x\":\"UVTi04QLrIuB7jXpVfHjUTVN5aIdcbPNr50umTtN8pw\"}" + ts-2025-10-B = "{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\"kid\":\"ts-2025-10-B\",\"use\":\"sig\",\"x\":\"HVTi04QLrIuB7jXpVfHjUTVN5aIdcbPNr50umTtN8pw\"}" + current-kid = "ts-2025-10-A" + active-kids = "ts-2025-10-A,ts-2025-10-B" diff --git a/crates/integration-tests/fixtures/frameworks/nextjs/Dockerfile b/crates/integration-tests/fixtures/frameworks/nextjs/Dockerfile new file mode 100644 index 00000000..37ecf03b --- /dev/null +++ b/crates/integration-tests/fixtures/frameworks/nextjs/Dockerfile @@ -0,0 +1,37 @@ +# Minimal Next.js container for integration testing. +# Builds a standalone Next.js 14 app with test pages containing ad slots. +# +# Build: +# docker build -t test-nextjs:latest \ +# crates/integration-tests/fixtures/frameworks/nextjs/ +# +# Run manually: +# docker run -p 3000:3000 test-nextjs:latest + +# --- Build stage --- +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package.json ./ +RUN npm install + +COPY next.config.mjs ./ +COPY app/ ./app/ + +RUN npm run build + +# --- Runtime stage --- +FROM node:20-alpine AS runner + +WORKDIR /app + +ENV NODE_ENV=production + +# Copy standalone output from builder +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/crates/integration-tests/fixtures/frameworks/nextjs/app/layout.tsx b/crates/integration-tests/fixtures/frameworks/nextjs/app/layout.tsx new file mode 100644 index 00000000..943adabd --- /dev/null +++ b/crates/integration-tests/fixtures/frameworks/nextjs/app/layout.tsx @@ -0,0 +1,17 @@ +export const metadata = { + title: "Test Publisher - Next.js", + description: "Integration test page for trusted server", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + ); +} diff --git a/crates/integration-tests/fixtures/frameworks/nextjs/app/page.tsx b/crates/integration-tests/fixtures/frameworks/nextjs/app/page.tsx new file mode 100644 index 00000000..093b815c --- /dev/null +++ b/crates/integration-tests/fixtures/frameworks/nextjs/app/page.tsx @@ -0,0 +1,20 @@ +export default function Home() { + return ( +
+

Integration Test Publisher

+

This is a test page for integration testing of the trusted server.

+ + {/* Ad slot that should be rewritten by the trusted server */} +
+

Advertisement placeholder

+
+ +

More page content follows the ad slot.

+ + {/* Second ad slot */} +
+

Sidebar advertisement placeholder

+
+
+ ); +} diff --git a/crates/integration-tests/fixtures/frameworks/nextjs/next.config.mjs b/crates/integration-tests/fixtures/frameworks/nextjs/next.config.mjs new file mode 100644 index 00000000..8c8bab67 --- /dev/null +++ b/crates/integration-tests/fixtures/frameworks/nextjs/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "standalone", +}; + +export default nextConfig; diff --git a/crates/integration-tests/fixtures/frameworks/nextjs/package.json b/crates/integration-tests/fixtures/frameworks/nextjs/package.json new file mode 100644 index 00000000..7c193fef --- /dev/null +++ b/crates/integration-tests/fixtures/frameworks/nextjs/package.json @@ -0,0 +1,15 @@ +{ + "name": "integration-test-nextjs", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start -p 3000" + }, + "dependencies": { + "next": "^14.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/crates/integration-tests/fixtures/frameworks/wordpress/Dockerfile b/crates/integration-tests/fixtures/frameworks/wordpress/Dockerfile new file mode 100644 index 00000000..c913cfa5 --- /dev/null +++ b/crates/integration-tests/fixtures/frameworks/wordpress/Dockerfile @@ -0,0 +1,22 @@ +# Minimal WordPress container for integration testing. +# Uses PHP built-in server with a simple test theme that outputs HTML +# with ad slot divs — no MySQL or full WordPress stack required. +# +# Build: +# docker build -t test-wordpress:latest \ +# crates/integration-tests/fixtures/frameworks/wordpress/ +# +# Run manually: +# docker run -p 8080:80 test-wordpress:latest + +FROM php:8.3-cli-alpine + +WORKDIR /var/www/html + +# Copy test theme files that simulate WordPress HTML output +COPY theme/ /var/www/html/ + +EXPOSE 80 + +# Serve with PHP built-in server +CMD ["php", "-S", "0.0.0.0:80", "-t", "/var/www/html"] diff --git a/crates/integration-tests/fixtures/frameworks/wordpress/theme/index.php b/crates/integration-tests/fixtures/frameworks/wordpress/theme/index.php new file mode 100644 index 00000000..624f4d28 --- /dev/null +++ b/crates/integration-tests/fixtures/frameworks/wordpress/theme/index.php @@ -0,0 +1,41 @@ + + + + + + Test Publisher - WordPress + + + +
+

Integration Test Publisher

+ +
+ +
+
+

Test Article

+

This is a test article for integration testing of the trusted server.

+ + +
+

Advertisement placeholder

+
+ +

More article content follows the ad slot.

+ + +
+

Sidebar advertisement placeholder

+
+
+
+ +
+

© 2025 Test Publisher

+
+ + diff --git a/crates/integration-tests/fixtures/frameworks/wordpress/theme/wp-admin/index.php b/crates/integration-tests/fixtures/frameworks/wordpress/theme/wp-admin/index.php new file mode 100644 index 00000000..23faf3e0 --- /dev/null +++ b/crates/integration-tests/fixtures/frameworks/wordpress/theme/wp-admin/index.php @@ -0,0 +1,13 @@ + + + + + WordPress Admin - Dashboard + + +
+

Dashboard

+

WordPress admin area — scripts should NOT be injected here.

+
+ + diff --git a/crates/integration-tests/tests/common/assertions.rs b/crates/integration-tests/tests/common/assertions.rs index bc943a18..7905eba7 100644 --- a/crates/integration-tests/tests/common/assertions.rs +++ b/crates/integration-tests/tests/common/assertions.rs @@ -1,32 +1,32 @@ use super::runtime::TestError; -use error_stack::{Result, ResultExt}; +use error_stack::Result; use scraper::{Html, Selector}; -/// Assert that HTML contains script tag with expected tsjs module reference +/// Parse a CSS selector, mapping the error to [`TestError::InvalidSelector`] +fn parse_selector(selector: &str) -> Result { + Selector::parse(selector).map_err(|_| error_stack::report!(TestError::InvalidSelector)) +} + +/// Assert that HTML contains a script tag with expected tsjs module reference. +/// +/// Looks for ` + "#; - assert_script_tag_present(html, &["core", "prebid"]) - .expect("should find script tag with expected modules"); + assert_script_tag_present(html).expect("should find trustedserver-js script tag"); } #[test] - fn script_tag_present_fails_on_missing_module() { + fn script_tag_present_fails_when_no_script() { + let html = r#" + + + "#; + + let result = assert_script_tag_present(html); + assert!(result.is_err(), "should fail when no script tag exists"); + } + + #[test] + fn script_tag_present_fails_when_wrong_script() { let html = r#" - + "#; - let result = assert_script_tag_present(html, &["core", "lockr"]); + let result = assert_script_tag_present(html); assert!( result.is_err(), - "should fail when expected module is missing" + "should fail when script tag is not tsjs" ); } - #[test] - fn script_tag_present_fails_when_no_script() { - let html = r#" - - - "#; - - let result = assert_script_tag_present(html, &["core"]); - assert!(result.is_err(), "should fail when no script tag exists"); - } - #[test] fn attribute_rewritten_with_expected_prefix() { let html = r#" diff --git a/crates/integration-tests/tests/common/config.rs b/crates/integration-tests/tests/common/config.rs deleted file mode 100644 index 8945a0c2..00000000 --- a/crates/integration-tests/tests/common/config.rs +++ /dev/null @@ -1,86 +0,0 @@ -use super::runtime::TestError; -use error_stack::{Result, ResultExt}; -use std::io::Write; -use std::path::PathBuf; -use tempfile::NamedTempFile; - -/// Builder for runtime configuration -pub struct RuntimeConfigBuilder { - template: String, - origin_url: Option, - integrations: Vec, - wasm_path: Option, -} - -impl RuntimeConfigBuilder { - pub fn new(template: &str) -> Self { - Self { - template: template.to_string(), - origin_url: None, - integrations: Vec::new(), - wasm_path: None, - } - } - - pub fn with_origin_url(mut self, url: String) -> Self { - self.origin_url = Some(url); - self - } - - pub fn with_integrations(mut self, ids: Vec<&str>) -> Self { - self.integrations = ids.iter().map(|s| s.to_string()).collect(); - self - } - - pub fn with_wasm_path(mut self, path: PathBuf) -> Self { - self.wasm_path = Some(path); - self - } - - pub fn build(self) -> Result { - // Parse template as TOML - let mut config: toml::Value = - toml::from_str(&self.template).change_context(TestError::ConfigParse)?; - - // Set origin_url if provided - if let Some(origin_url) = self.origin_url { - if let Some(publisher) = config.get_mut("publisher") { - if let Some(publisher_table) = publisher.as_table_mut() { - publisher_table - .insert("origin_url".to_string(), toml::Value::String(origin_url)); - } - } - } - - // Enable specified integrations - // Config structure: [integrations.] with `enabled = true/false` - if !self.integrations.is_empty() { - if let Some(integrations_table) = config - .get_mut("integrations") - .and_then(|v| v.as_table_mut()) - { - for integration_id in &self.integrations { - if let Some(integration_entry) = - integrations_table.get_mut(integration_id.as_str()) - { - if let Some(table) = integration_entry.as_table_mut() { - table.insert("enabled".to_string(), toml::Value::Boolean(true)); - } - } - } - } - } - - // Write to temp file - let mut file = NamedTempFile::new().change_context(TestError::ConfigWrite)?; - let content = toml::to_string_pretty(&config).change_context(TestError::ConfigSerialize)?; - file.write_all(content.as_bytes()) - .change_context(TestError::ConfigWrite)?; - - let wasm_path = self - .wasm_path - .unwrap_or_else(super::runtime::wasm_binary_path); - - Ok(super::RuntimeConfig::new(file, wasm_path)) - } -} diff --git a/crates/integration-tests/tests/common/mod.rs b/crates/integration-tests/tests/common/mod.rs index af63a198..86cd5f62 100644 --- a/crates/integration-tests/tests/common/mod.rs +++ b/crates/integration-tests/tests/common/mod.rs @@ -1,7 +1,5 @@ pub mod assertions; -pub mod config; pub mod runtime; pub use assertions::*; -pub use config::*; pub use runtime::*; diff --git a/crates/integration-tests/tests/common/runtime.rs b/crates/integration-tests/tests/common/runtime.rs index fb770fb6..4b92dd81 100644 --- a/crates/integration-tests/tests/common/runtime.rs +++ b/crates/integration-tests/tests/common/runtime.rs @@ -1,7 +1,6 @@ use error_stack::Result; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use tempfile::NamedTempFile; /// Test error types (platform-agnostic) #[derive(Debug, derive_more::Display)] @@ -37,9 +36,6 @@ pub enum TestError { #[display("Script tag not found in HTML")] ScriptTagNotFound, - #[display("Missing module: {module}")] - MissingModule { module: String }, - #[display("Invalid CSS selector")] InvalidSelector, @@ -52,55 +48,13 @@ pub enum TestError { #[display("GDPR signal missing from response")] GdprSignalMissing, - // Configuration errors - #[display("Config parse error")] - ConfigParse, - - #[display("Config write error")] - ConfigWrite, - - #[display("Config serialization error")] - ConfigSerialize, - // Resource errors - #[display("WASM binary not found")] - WasmBinaryNotFound, - #[display("No available port found")] NoPortAvailable, } impl core::error::Error for TestError {} -/// Configuration for runtime environments. -/// -/// Holds the temp file so the config is not deleted while the runtime is alive. -pub struct RuntimeConfig { - /// Handle to the temp config file — dropped when `RuntimeConfig` is dropped. - _config_file: NamedTempFile, - wasm_path: PathBuf, -} - -impl RuntimeConfig { - /// Create a new runtime configuration from a temp file and WASM binary path. - pub fn new(config_file: NamedTempFile, wasm_path: PathBuf) -> Self { - Self { - _config_file: config_file, - wasm_path, - } - } - - /// Path to the generated config file on disk. - pub fn config_path(&self) -> &Path { - self._config_file.path() - } - - /// Path to the WASM binary. - pub fn wasm_path(&self) -> &Path { - &self.wasm_path - } -} - /// Platform-agnostic process handle pub struct RuntimeProcess { pub inner: Box, @@ -113,16 +67,23 @@ pub trait RuntimeProcessHandle: Send + Sync { fn wait(&mut self) -> Result<(), TestError>; } -/// Trait defining how to run the trusted-server on different platforms +/// Trait defining how to run the trusted-server on different platforms. +/// +/// The application configuration (origin URL, integrations, etc.) is baked +/// into the WASM binary at build time via `build.rs`. The runtime environment +/// only needs the WASM binary path and its own platform-specific config +/// (e.g. Viceroy's `fastly.toml` for KV stores and secret stores). pub trait RuntimeEnvironment: Send + Sync { /// Platform identifier (e.g., "fastly", "cloudflare") fn id(&self) -> &'static str; - /// Spawn runtime with platform-specific configuration - fn spawn(&self, config: &RuntimeConfig) -> Result; - - /// Platform-specific configuration template - fn config_template(&self) -> &str; + /// Spawn runtime with the given WASM binary. + /// + /// # Errors + /// + /// Returns [`TestError::RuntimeSpawn`] if the process cannot be started. + /// Returns [`TestError::RuntimeNotReady`] if the health check times out. + fn spawn(&self, wasm_path: &Path) -> Result; /// Health check endpoint (may differ by platform) fn health_check_path(&self) -> &str { @@ -135,7 +96,7 @@ pub trait RuntimeEnvironment: Send + Sync { } } -/// Get path to WASM binary, respecting environment variable +/// Get path to WASM binary, respecting environment variable. pub fn wasm_binary_path() -> PathBuf { std::env::var("WASM_BINARY_PATH") .map(PathBuf::from) @@ -144,3 +105,14 @@ pub fn wasm_binary_path() -> PathBuf { .join("../../target/wasm32-wasip1/release/trusted-server-fastly.wasm") }) } + +/// Get the fixed origin port used for Docker container port mapping. +/// +/// This must match the port baked into the WASM binary via +/// `TRUSTED_SERVER__PUBLISHER__ORIGIN_URL` at build time. +pub fn origin_port() -> u16 { + std::env::var("INTEGRATION_ORIGIN_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(8888) +} diff --git a/crates/integration-tests/tests/environments/fastly.rs b/crates/integration-tests/tests/environments/fastly.rs index f0301bab..35328ef5 100644 --- a/crates/integration-tests/tests/environments/fastly.rs +++ b/crates/integration-tests/tests/environments/fastly.rs @@ -1,14 +1,14 @@ -use crate::common::runtime::{ - RuntimeConfig, RuntimeEnvironment, RuntimeProcess, RuntimeProcessHandle, TestError, -}; +use crate::common::runtime::{RuntimeEnvironment, RuntimeProcess, RuntimeProcessHandle, TestError}; use error_stack::ResultExt as _; -use std::collections::HashMap; +use std::path::Path; use std::process::{Child, Command}; /// Fastly Compute runtime using Viceroy local simulator. /// -/// Spawns a `viceroy` child process with the WASM binary and a generated -/// `fastly.toml` config. Dynamic port allocation allows parallel test execution. +/// Spawns a `viceroy` child process with the WASM binary and the +/// Viceroy-specific `fastly.toml` config (KV stores, secrets). +/// The application config (origin URL, integrations) is baked into +/// the WASM binary at build time. pub struct FastlyViceroy; impl RuntimeEnvironment for FastlyViceroy { @@ -16,21 +16,17 @@ impl RuntimeEnvironment for FastlyViceroy { "fastly" } - fn spawn(&self, config: &RuntimeConfig) -> error_stack::Result { + fn spawn(&self, wasm_path: &Path) -> error_stack::Result { let port = super::find_available_port()?; - // Viceroy requires a fastly.toml for local_server config (KV stores, secrets). - // The app config (trusted-server.toml) is read by the WASM binary itself via - // environment variable or compiled-in path. let viceroy_config = self.viceroy_config_path(); let child = Command::new("viceroy") - .arg(config.wasm_path()) + .arg(wasm_path) .arg("-C") .arg(&viceroy_config) .arg("--addr") .arg(format!("127.0.0.1:{port}")) - .env("TRUSTED_SERVER_CONFIG", config.config_path()) .spawn() .change_context(TestError::RuntimeSpawn) .attach_printable("Failed to spawn viceroy process")?; @@ -45,17 +41,9 @@ impl RuntimeEnvironment for FastlyViceroy { }) } - fn config_template(&self) -> &str { - include_str!("../../fixtures/configs/fastly-template.toml") - } - fn health_check_path(&self) -> &str { "/health" } - - fn env_vars(&self) -> HashMap { - HashMap::new() - } } impl FastlyViceroy { diff --git a/crates/integration-tests/tests/environments/mod.rs b/crates/integration-tests/tests/environments/mod.rs index 0f4a7ab9..5615b72a 100644 --- a/crates/integration-tests/tests/environments/mod.rs +++ b/crates/integration-tests/tests/environments/mod.rs @@ -18,8 +18,6 @@ type RuntimeFactory = fn() -> Box; /// 3. Add factory closure here pub static RUNTIME_ENVIRONMENTS: &[RuntimeFactory] = &[ || Box::new(fastly::FastlyViceroy), - // Future: Add Cloudflare, Fermyon Spin, etc. - // || Box::new(cloudflare::CloudflareWrangler), ]; /// Find an available TCP port for the runtime to bind to. diff --git a/crates/integration-tests/tests/frameworks/mod.rs b/crates/integration-tests/tests/frameworks/mod.rs index 78de4832..3dc48e77 100644 --- a/crates/integration-tests/tests/frameworks/mod.rs +++ b/crates/integration-tests/tests/frameworks/mod.rs @@ -4,6 +4,7 @@ pub mod wordpress; use crate::common::runtime::TestError; use scenarios::{CustomScenario, TestScenario}; +use testcontainers::core::ContainerRequest; use testcontainers::GenericImage; /// Trait defining how to test a frontend framework. @@ -21,12 +22,18 @@ pub trait FrontendFramework: Send + Sync { /// Framework identifier (e.g. "wordpress", "nextjs"). fn id(&self) -> &'static str; - /// Build a Docker container image for this framework. + /// Build a Docker container request mapped to the given origin port. + /// + /// The `origin_port` is the fixed host port that the WASM binary + /// expects the origin to be running on (baked in at build time). /// /// # Errors /// /// Returns [`TestError::ContainerStart`] if the image cannot be created. - fn build_container(&self) -> error_stack::Result; + fn build_container( + &self, + origin_port: u16, + ) -> error_stack::Result, TestError>; /// Port the framework serves on inside the container. fn container_port(&self) -> u16; diff --git a/crates/integration-tests/tests/frameworks/nextjs.rs b/crates/integration-tests/tests/frameworks/nextjs.rs index 1d3f1cf0..f1ee8a41 100644 --- a/crates/integration-tests/tests/frameworks/nextjs.rs +++ b/crates/integration-tests/tests/frameworks/nextjs.rs @@ -1,7 +1,8 @@ use super::FrontendFramework; use super::scenarios::{CustomScenario, TestScenario}; use crate::common::runtime::TestError; -use testcontainers::GenericImage; +use testcontainers::core::{ContainerRequest, IntoContainerPort}; +use testcontainers::{GenericImage, ImageExt as _}; /// Next.js frontend framework for integration testing. /// @@ -19,9 +20,14 @@ impl FrontendFramework for NextJs { "nextjs" } - fn build_container(&self) -> error_stack::Result { + fn build_container( + &self, + origin_port: u16, + ) -> error_stack::Result, TestError> { + let container_port = self.container_port(); Ok(GenericImage::new("test-nextjs", "latest") - .with_exposed_port(testcontainers::core::IntoContainerPort::tcp(3000))) + .with_exposed_port(container_port.tcp()) + .with_mapped_port(origin_port, container_port.tcp())) } fn container_port(&self) -> u16 { @@ -35,11 +41,7 @@ impl FrontendFramework for NextJs { fn standard_scenarios(&self) -> Vec { vec![ TestScenario::HtmlInjection, - TestScenario::ScriptServing { - modules: vec!["core", "prebid", "lockr"], - }, - TestScenario::AttributeRewriting, - TestScenario::GdprSignal, + TestScenario::ScriptServing, ] } diff --git a/crates/integration-tests/tests/frameworks/scenarios.rs b/crates/integration-tests/tests/frameworks/scenarios.rs index b341f93d..732b2abd 100644 --- a/crates/integration-tests/tests/frameworks/scenarios.rs +++ b/crates/integration-tests/tests/frameworks/scenarios.rs @@ -11,8 +11,8 @@ pub enum TestScenario { /// Verify ` + + + + "#; + + assert_unique_script_tag(html).expect("should find exactly one trustedserver-js"); + } + + #[test] + fn unique_script_tag_fails_when_missing() { + let html = r#" + + + "#; + + let result = assert_unique_script_tag(html); + assert!(result.is_err(), "should fail when no trustedserver-js exists"); + } + + #[test] + fn unique_script_tag_fails_when_duplicated() { + let html = r#" + + + + + + + + + "#; + + let result = assert_unique_script_tag(html); + assert!(result.is_err(), "should fail when multiple trustedserver-js exist"); + } + + #[test] + fn unique_script_tag_fails_when_no_id() { + let html = r#" + + + + + + + + "#; + + let result = assert_unique_script_tag(html); + assert!(result.is_err(), "should fail when script has no id"); + } + + #[test] + fn data_ad_units_preserved_passes() { + let html = r#" + + + +
Ad
+
Ad
+ + + "#; + + assert_data_ad_units_preserved(html, &["/test/banner", "/test/sidebar"]) + .expect("should find both ad units"); + } + + #[test] + fn data_ad_units_preserved_fails_when_missing() { + let html = r#" + + + +
Ad
+ + + "#; + + let result = assert_data_ad_units_preserved(html, &["/test/banner", "/test/sidebar"]); + assert!(result.is_err(), "should fail when expected ad unit is missing"); + } + #[test] fn script_tag_present_fails_when_wrong_script() { let html = r#" diff --git a/crates/integration-tests/tests/common/runtime.rs b/crates/integration-tests/tests/common/runtime.rs index 02bae534..09c742d0 100644 --- a/crates/integration-tests/tests/common/runtime.rs +++ b/crates/integration-tests/tests/common/runtime.rs @@ -39,6 +39,9 @@ pub enum TestError { #[display("Invalid CSS selector")] InvalidSelector, + #[display("Origin URL not rewritten in HTML attributes")] + AttributeNotRewritten, + // Resource errors #[display("No available port found")] NoPortAvailable, diff --git a/crates/integration-tests/tests/environments/fastly.rs b/crates/integration-tests/tests/environments/fastly.rs index 35328ef5..48aab905 100644 --- a/crates/integration-tests/tests/environments/fastly.rs +++ b/crates/integration-tests/tests/environments/fastly.rs @@ -31,18 +31,20 @@ impl RuntimeEnvironment for FastlyViceroy { .change_context(TestError::RuntimeSpawn) .attach_printable("Failed to spawn viceroy process")?; + // Wrap immediately so Drop::drop kills the process if readiness check fails + let handle = ViceroyHandle { child }; let base_url = format!("http://127.0.0.1:{port}"); super::wait_for_ready(&base_url, self.health_check_path())?; Ok(RuntimeProcess { - inner: Box::new(ViceroyHandle { child }), + inner: Box::new(handle), base_url, }) } fn health_check_path(&self) -> &str { - "/health" + "/__trusted-server/health" } } diff --git a/crates/integration-tests/tests/environments/mod.rs b/crates/integration-tests/tests/environments/mod.rs index aef6428f..23ccc175 100644 --- a/crates/integration-tests/tests/environments/mod.rs +++ b/crates/integration-tests/tests/environments/mod.rs @@ -50,17 +50,17 @@ pub fn wait_for_ready(base_url: &str, health_path: &str) -> error_stack::Result< let health_url = format!("{}{}", base_url, health_path); for _ in 0..30 { - if let Ok(resp) = reqwest::blocking::get(&health_url) { - if resp.status().is_success() { - return Ok(()); - } + if let Ok(resp) = reqwest::blocking::get(&health_url) + && resp.status().is_success() + { + return Ok(()); } // Fallback: try root path — a 404 means the server is responsive - if let Ok(resp) = reqwest::blocking::get(base_url) { - if resp.status().is_success() || resp.status().as_u16() == 404 { - return Ok(()); - } + if let Ok(resp) = reqwest::blocking::get(base_url) + && (resp.status().is_success() || resp.status().as_u16() == 404) + { + return Ok(()); } std::thread::sleep(std::time::Duration::from_millis(100)); diff --git a/crates/integration-tests/tests/frameworks/nextjs.rs b/crates/integration-tests/tests/frameworks/nextjs.rs index 16bb0db3..27776b4b 100644 --- a/crates/integration-tests/tests/frameworks/nextjs.rs +++ b/crates/integration-tests/tests/frameworks/nextjs.rs @@ -25,9 +25,11 @@ impl FrontendFramework for NextJs { origin_port: u16, ) -> error_stack::Result, TestError> { let container_port = self.container_port(); + let origin_host = format!("127.0.0.1:{origin_port}"); Ok(GenericImage::new("test-nextjs", "latest") .with_exposed_port(container_port.tcp()) - .with_mapped_port(origin_port, container_port.tcp())) + .with_mapped_port(origin_port, container_port.tcp()) + .with_env_var("ORIGIN_HOST", origin_host)) } fn container_port(&self) -> u16 { @@ -39,7 +41,12 @@ impl FrontendFramework for NextJs { } fn standard_scenarios(&self) -> Vec { - vec![TestScenario::HtmlInjection, TestScenario::ScriptServing] + vec![ + TestScenario::HtmlInjection, + TestScenario::ScriptServing, + TestScenario::AttributeRewriting, + TestScenario::ScriptServingUnknownFile404, + ] } fn custom_scenarios(&self) -> Vec { diff --git a/crates/integration-tests/tests/frameworks/scenarios.rs b/crates/integration-tests/tests/frameworks/scenarios.rs index 3c340b7f..b3a4ff5b 100644 --- a/crates/integration-tests/tests/frameworks/scenarios.rs +++ b/crates/integration-tests/tests/frameworks/scenarios.rs @@ -1,5 +1,5 @@ use crate::common::assertions; -use crate::common::runtime::TestError; +use crate::common::runtime::{TestError, origin_port}; use error_stack::ResultExt as _; /// Standard test scenarios applicable to all frontend frameworks. @@ -13,6 +13,12 @@ pub enum TestScenario { /// Verify `/static/tsjs=tsjs-unified.min.js` endpoint serves the JS bundle. ScriptServing, + + /// Verify origin host URLs in `href`/`src` attributes are rewritten to the proxy host. + AttributeRewriting, + + /// Verify `/static/tsjs=unknown.js` returns 404, not HTML or a fallback. + ScriptServingUnknownFile404, } /// Framework-specific custom scenarios that test framework-unique behaviors. @@ -23,6 +29,13 @@ pub enum CustomScenario { /// Next.js: Server Actions POST requests pass through correctly. NextJsServerActions, + + /// WordPress: Admin pages (`/wp-admin/`) receive script injection. + /// + /// The trusted server currently injects into ALL HTML responses + /// regardless of path. This test documents that behavior and guards + /// against unintended changes. + WordPressAdminInjection, } impl TestScenario { @@ -50,9 +63,40 @@ impl TestScenario { .change_context(TestError::ResponseParse) .attach_printable(format!("framework: {framework_id}"))?; - assertions::assert_script_tag_present(&html) + assertions::assert_unique_script_tag(&html) + .attach_printable(format!("framework: {framework_id}"))?; + + Ok(()) + } + + Self::AttributeRewriting => { + // Verify that absolute origin URLs in href/src attributes are + // rewritten to the proxy host. The test fixtures embed links + // like `http://127.0.0.1:8888/page` which the HTML processor + // should rewrite to `http://127.0.0.1:{proxy_port}/page`. + let resp = reqwest::blocking::get(base_url) + .change_context(TestError::HttpRequest) + .attach_printable(format!( + "scenario: AttributeRewriting, framework: {framework_id}" + ))?; + + let html = resp + .text() + .change_context(TestError::ResponseParse) + .attach_printable(format!("framework: {framework_id}"))?; + + let origin_host = format!("127.0.0.1:{}", origin_port()); + + assertions::assert_attributes_rewritten(&html, &origin_host, base_url) .attach_printable(format!("framework: {framework_id}"))?; + // Verify non-URL attributes like data-ad-unit are preserved unchanged + assertions::assert_data_ad_units_preserved( + &html, + &["/test/banner", "/test/sidebar"], + ) + .attach_printable(format!("framework: {framework_id}"))?; + Ok(()) } @@ -74,6 +118,75 @@ impl TestScenario { ); } + // Verify content type is JavaScript + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + + let body = resp + .text() + .change_context(TestError::ResponseParse) + .attach_printable(format!("framework: {framework_id}"))?; + + if !content_type.contains("javascript") { + return Err(error_stack::report!(TestError::ResponseParse) + .attach_printable(format!( + "Expected JavaScript content-type, got: {content_type}" + ))); + } + + // Verify body is non-empty and contains expected bundle markers + if body.is_empty() { + return Err(error_stack::report!(TestError::ResponseParse) + .attach_printable("Script bundle body is empty")); + } + + // The unified bundle should contain the TSJS core initialization + if !body.contains("trustedserver") && !body.contains("tsjs") { + return Err(error_stack::report!(TestError::ResponseParse) + .attach_printable( + "Script bundle does not contain expected trustedserver/tsjs markers", + )); + } + + Ok(()) + } + + Self::ScriptServingUnknownFile404 => { + let url = format!("{base_url}/static/tsjs=unknown.js"); + + let resp = reqwest::blocking::get(&url) + .change_context(TestError::HttpRequest) + .attach_printable(format!( + "scenario: ScriptServingUnknownFile404, framework: {framework_id}" + ))?; + + let status = resp.status().as_u16(); + + if status != 404 { + return Err(error_stack::report!(TestError::HttpRequest) + .attach_printable(format!( + "Expected 404 for unknown tsjs file, got {status}" + ))); + } + + // Response should not be HTML (which would indicate a fallback to origin) + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + if content_type.contains("text/html") { + return Err(error_stack::report!(TestError::ResponseParse) + .attach_printable( + "Unknown tsjs file returned HTML instead of a proper 404", + )); + } + Ok(()) } } @@ -89,7 +202,9 @@ impl CustomScenario { pub fn run(&self, base_url: &str, framework_id: &str) -> error_stack::Result<(), TestError> { match self { Self::NextJsRscFlight => { - // Verify RSC Flight format responses are not corrupted + // Verify RSC Flight format responses are not corrupted. + // When the proxy mishandles the RSC header, it returns text/html + // instead of the expected Flight payload — so we fail on HTML. let client = reqwest::blocking::Client::new(); let resp = client .get(base_url) @@ -105,27 +220,41 @@ impl CustomScenario { .headers() .get("content-type") .and_then(|v| v.to_str().ok()) - .unwrap_or(""); + .unwrap_or("") + .to_string(); - // RSC responses should have text/x-component content type - // and should NOT have script injection (they're not HTML) - if content_type.contains("text/x-component") { - let body = resp.text().change_context(TestError::ResponseParse)?; - - if body.contains("/static/tsjs=") { - return Err(error_stack::report!(TestError::ScriptTagNotFound) - .attach_printable( - "Script tag should NOT be injected in RSC Flight responses", - )); - } + let body = resp.text().change_context(TestError::ResponseParse)?; + + // RSC responses must NOT be HTML — that means the proxy + // swallowed the RSC header and treated it as a page request + if content_type.contains("text/html") { + return Err(error_stack::report!(TestError::ResponseParse) + .attach_printable(format!( + "RSC request returned text/html instead of Flight payload (content-type: {content_type})" + ))); + } + + // If the response is a Flight payload, it must not contain injected scripts + if body.contains("/static/tsjs=") { + return Err(error_stack::report!(TestError::ScriptTagNotFound) + .attach_printable( + "Script tag should NOT be injected in RSC Flight responses", + )); } Ok(()) } Self::NextJsServerActions => { - // Verify POST requests for server actions pass through correctly - let client = reqwest::blocking::Client::new(); + // Verify POST requests pass through the proxy to the origin. + // The minimal Next.js app has no real server actions, so the + // framework returns a client or redirect response. A 5xx means + // the proxy itself failed rather than forwarding. + let client = reqwest::blocking::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .change_context(TestError::HttpRequest)?; + let resp = client .post(base_url) .header("Next-Action", "test-action-id") @@ -137,20 +266,49 @@ impl CustomScenario { "scenario: NextJsServerActions, framework: {framework_id}" ))?; - // Server action responses should pass through without modification - // A 4xx is acceptable since we don't have real server actions, - // but a connection error would indicate the proxy is blocking POSTs - if resp.status().is_server_error() { + let status = resp.status(); + + // The proxy must forward the POST. We expect a non-5xx from + // the origin: 2xx, 3xx (redirect), or 4xx (no matching action). + if status.is_server_error() { + let body = resp.text().unwrap_or_default(); return Err( error_stack::report!(TestError::HttpRequest).attach_printable(format!( - "Server action request failed with {}", - resp.status() + "POST request failed with {status}; body: {body}" )), ); } Ok(()) } + + Self::WordPressAdminInjection => { + // Verify that /wp-admin/ pages also receive script injection. + // The trusted server injects into ALL HTML responses regardless + // of path. This test documents that behavior — if admin-path + // exclusion is added in the future, this test should be updated + // to assert NO injection instead. + let url = format!("{base_url}/wp-admin/"); + + let resp = reqwest::blocking::get(&url) + .change_context(TestError::HttpRequest) + .attach_printable(format!( + "scenario: WordPressAdminInjection, framework: {framework_id}" + ))?; + + let html = resp + .text() + .change_context(TestError::ResponseParse) + .attach_printable(format!("framework: {framework_id}"))?; + + assertions::assert_script_tag_present(&html) + .attach_printable(format!( + "Admin page should receive injection (current behavior). \ + framework: {framework_id}" + ))?; + + Ok(()) + } } } } diff --git a/crates/integration-tests/tests/frameworks/wordpress.rs b/crates/integration-tests/tests/frameworks/wordpress.rs index faad4a42..0cdfe52e 100644 --- a/crates/integration-tests/tests/frameworks/wordpress.rs +++ b/crates/integration-tests/tests/frameworks/wordpress.rs @@ -26,9 +26,11 @@ impl FrontendFramework for WordPress { origin_port: u16, ) -> error_stack::Result, TestError> { let container_port = self.container_port(); + let origin_host = format!("127.0.0.1:{origin_port}"); Ok(GenericImage::new("test-wordpress", "latest") .with_exposed_port(container_port.tcp()) - .with_mapped_port(origin_port, container_port.tcp())) + .with_mapped_port(origin_port, container_port.tcp()) + .with_env_var("ORIGIN_HOST", origin_host)) } fn container_port(&self) -> u16 { @@ -40,6 +42,15 @@ impl FrontendFramework for WordPress { } fn standard_scenarios(&self) -> Vec { - vec![TestScenario::HtmlInjection, TestScenario::ScriptServing] + vec![ + TestScenario::HtmlInjection, + TestScenario::ScriptServing, + TestScenario::AttributeRewriting, + TestScenario::ScriptServingUnknownFile404, + ] + } + + fn custom_scenarios(&self) -> Vec { + vec![CustomScenario::WordPressAdminInjection] } } diff --git a/crates/integration-tests/tests/integration.rs b/crates/integration-tests/tests/integration.rs index eaa59b84..36b5be1c 100644 --- a/crates/integration-tests/tests/integration.rs +++ b/crates/integration-tests/tests/integration.rs @@ -108,10 +108,10 @@ fn wait_for_container(base_url: &str, health_path: &str) -> error_stack::Result< let url = format!("{base_url}{health_path}"); for _ in 0..60 { - if let Ok(resp) = reqwest::blocking::get(&url) { - if resp.status().is_success() { - return Ok(()); - } + if let Ok(resp) = reqwest::blocking::get(&url) + && resp.status().is_success() + { + return Ok(()); } std::thread::sleep(Duration::from_millis(500)); } diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh index b5e89bdd..316d416c 100755 --- a/scripts/integration-tests.sh +++ b/scripts/integration-tests.sh @@ -20,12 +20,12 @@ cd "$REPO_ROOT" # can proxy requests to them. ORIGIN_PORT="${INTEGRATION_ORIGIN_PORT:-8888}" -# Detect native target -case "$(uname -m)" in - arm64|aarch64) TARGET="aarch64-apple-darwin" ;; - x86_64) TARGET="x86_64-unknown-linux-gnu" ;; - *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; -esac +# Detect native target from rustc (handles all OS + arch combinations correctly) +TARGET="$(rustc -vV | sed -n 's/^host: //p')" +if [ -z "$TARGET" ]; then + echo "Failed to detect host target from rustc -vV" >&2 + exit 1 +fi echo "==> Building WASM binary (origin=http://127.0.0.1:$ORIGIN_PORT)..." TRUSTED_SERVER__PUBLISHER__ORIGIN_URL="http://127.0.0.1:$ORIGIN_PORT" \ @@ -44,4 +44,7 @@ echo "==> Running integration tests (target: $TARGET, origin port: $ORIGIN_PORT) WASM_BINARY_PATH="$REPO_ROOT/target/wasm32-wasip1/release/trusted-server-fastly.wasm" \ INTEGRATION_ORIGIN_PORT="$ORIGIN_PORT" \ RUST_LOG=info \ - cargo test -p integration-tests --target "$TARGET" -- --include-ignored --test-threads=1 "$@" + cargo test \ + --manifest-path crates/integration-tests/Cargo.toml \ + --target "$TARGET" \ + -- --include-ignored --test-threads=1 "$@" From 582f80c91b79697485f565603762da948140384b Mon Sep 17 00:00:00 2001 From: prk-Jr Date: Sat, 7 Mar 2026 13:13:16 +0530 Subject: [PATCH 07/15] Enrich Next.js fixture and add browser integration tests --- .github/workflows/integration-tests.yml | 98 ++++++++ .gitignore | 8 +- crates/integration-tests/Cargo.lock | 1 + crates/integration-tests/Cargo.toml | 1 + crates/integration-tests/README.md | 100 ++++++-- .../integration-tests/browser/global-setup.ts | 80 +++++++ .../browser/global-teardown.ts | 33 +++ .../browser/helpers/infra.ts | 128 ++++++++++ .../browser/helpers/state.ts | 16 ++ .../browser/helpers/wait-for-ready.ts | 30 +++ .../browser/package-lock.json | 78 ++++++ crates/integration-tests/browser/package.json | 13 + .../browser/playwright.config.ts | 25 ++ .../tests/nextjs/api-passthrough.spec.ts | 40 ++++ .../tests/nextjs/form-rewriting.spec.ts | 33 +++ .../browser/tests/nextjs/navigation.spec.ts | 99 ++++++++ .../tests/shared/script-bundle.spec.ts | 53 +++++ .../tests/shared/script-injection.spec.ts | 36 +++ .../tests/wordpress/admin-injection.spec.ts | 22 ++ .../integration-tests/browser/tsconfig.json | 12 + .../fixtures/frameworks/nextjs/Dockerfile | 4 +- .../frameworks/nextjs/app/about/page.tsx | 18 ++ .../frameworks/nextjs/app/api/data/route.ts | 11 + .../frameworks/nextjs/app/api/hello/route.ts | 9 + .../nextjs/app/components/Navigation.tsx | 21 ++ .../frameworks/nextjs/app/contact/page.tsx | 28 +++ .../frameworks/nextjs/app/dashboard/page.tsx | 30 +++ .../fixtures/frameworks/nextjs/app/layout.tsx | 7 +- .../fixtures/frameworks/nextjs/app/page.tsx | 8 +- .../fixtures/frameworks/nextjs/package.json | 5 + .../frameworks/wordpress/theme/index.php | 8 +- .../tests/common/assertions.rs | 222 ++++++++++++++++++ .../tests/frameworks/nextjs.rs | 2 + .../tests/frameworks/scenarios.rs | 137 ++++++++++- scripts/integration-tests-browser.sh | 63 +++++ 35 files changed, 1444 insertions(+), 35 deletions(-) create mode 100644 crates/integration-tests/browser/global-setup.ts create mode 100644 crates/integration-tests/browser/global-teardown.ts create mode 100644 crates/integration-tests/browser/helpers/infra.ts create mode 100644 crates/integration-tests/browser/helpers/state.ts create mode 100644 crates/integration-tests/browser/helpers/wait-for-ready.ts create mode 100644 crates/integration-tests/browser/package-lock.json create mode 100644 crates/integration-tests/browser/package.json create mode 100644 crates/integration-tests/browser/playwright.config.ts create mode 100644 crates/integration-tests/browser/tests/nextjs/api-passthrough.spec.ts create mode 100644 crates/integration-tests/browser/tests/nextjs/form-rewriting.spec.ts create mode 100644 crates/integration-tests/browser/tests/nextjs/navigation.spec.ts create mode 100644 crates/integration-tests/browser/tests/shared/script-bundle.spec.ts create mode 100644 crates/integration-tests/browser/tests/shared/script-injection.spec.ts create mode 100644 crates/integration-tests/browser/tests/wordpress/admin-injection.spec.ts create mode 100644 crates/integration-tests/browser/tsconfig.json create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/app/about/page.tsx create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/app/api/data/route.ts create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/app/api/hello/route.ts create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/app/components/Navigation.tsx create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/app/contact/page.tsx create mode 100644 crates/integration-tests/fixtures/frameworks/nextjs/app/dashboard/page.tsx create mode 100755 scripts/integration-tests-browser.sh diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d9a4aa08..f1941940 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -77,3 +77,101 @@ jobs: WASM_BINARY_PATH: target/wasm32-wasip1/release/trusted-server-fastly.wasm INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }} RUST_LOG: info + + browser-tests: + name: browser integration tests + runs-on: ubuntu-latest + if: >- + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') + steps: + - uses: actions/checkout@v4 + + - name: Retrieve Rust version + id: rust-version + run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + shell: bash + + - name: Set up Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ steps.rust-version.outputs.rust-version }} + target: wasm32-wasip1 + cache-shared-key: cargo-${{ runner.os }} + + - name: Get Viceroy cache key + id: viceroy-rev + run: echo "sha=$(git ls-remote https://github.com/fastly/Viceroy HEAD | cut -f1)" >> $GITHUB_OUTPUT + + - name: Cache Viceroy binary + id: cache-viceroy + uses: actions/cache@v4 + with: + path: ~/.cargo/bin/viceroy + key: viceroy-${{ runner.os }}-${{ steps.viceroy-rev.outputs.sha }} + + - name: Install Viceroy + if: steps.cache-viceroy.outputs.cache-hit != 'true' + run: cargo install --git https://github.com/fastly/Viceroy viceroy + + - name: Build WASM binary + run: cargo build --bin trusted-server-fastly --release --target wasm32-wasip1 + env: + TRUSTED_SERVER__PUBLISHER__ORIGIN_URL: "http://127.0.0.1:${{ env.ORIGIN_PORT }}" + TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false" + + - name: Build WordPress test container + run: | + docker build -t test-wordpress:latest \ + crates/integration-tests/fixtures/frameworks/wordpress/ + + - name: Build Next.js test container + run: | + docker build -t test-nextjs:latest \ + crates/integration-tests/fixtures/frameworks/nextjs/ + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install Playwright + working-directory: crates/integration-tests/browser + run: | + npm ci + npx playwright install --with-deps chromium + + - name: Run browser tests (Next.js) + working-directory: crates/integration-tests/browser + env: + WASM_BINARY_PATH: ${{ github.workspace }}/target/wasm32-wasip1/release/trusted-server-fastly.wasm + INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }} + VICEROY_CONFIG_PATH: ${{ github.workspace }}/crates/integration-tests/fixtures/configs/viceroy-template.toml + TEST_FRAMEWORK: nextjs + run: npx playwright test + + - name: Run browser tests (WordPress) + working-directory: crates/integration-tests/browser + env: + WASM_BINARY_PATH: ${{ github.workspace }}/target/wasm32-wasip1/release/trusted-server-fastly.wasm + INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }} + VICEROY_CONFIG_PATH: ${{ github.workspace }}/crates/integration-tests/fixtures/configs/viceroy-template.toml + TEST_FRAMEWORK: wordpress + run: npx playwright test + + - name: Upload Playwright HTML report + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: crates/integration-tests/browser/playwright-report/ + retention-days: 7 + + - name: Upload Playwright traces and screenshots + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-traces + path: crates/integration-tests/browser/test-results/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index deccb58e..864be6a1 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,10 @@ src/*.html *.pem /guest-profiles -/benchmark-results/** \ No newline at end of file +/benchmark-results/** + +# Playwright browser tests +/crates/integration-tests/browser/node_modules/ +/crates/integration-tests/browser/test-results/ +/crates/integration-tests/browser/playwright-report/ +/crates/integration-tests/browser/.browser-test-state.json \ No newline at end of file diff --git a/crates/integration-tests/Cargo.lock b/crates/integration-tests/Cargo.lock index 60cee858..26bcdf0d 100644 --- a/crates/integration-tests/Cargo.lock +++ b/crates/integration-tests/Cargo.lock @@ -1133,6 +1133,7 @@ dependencies = [ "log", "reqwest", "scraper", + "serde_json", "testcontainers", ] diff --git a/crates/integration-tests/Cargo.toml b/crates/integration-tests/Cargo.toml index 962c87e4..508da391 100644 --- a/crates/integration-tests/Cargo.toml +++ b/crates/integration-tests/Cargo.toml @@ -14,5 +14,6 @@ testcontainers = { version = "0.25", features = ["blocking"] } reqwest = { version = "0.12", features = ["blocking"] } scraper = "0.21" log = "0.4" +serde_json = "1" error-stack = "0.5" derive_more = { version = "1.0", features = ["display"] } diff --git a/crates/integration-tests/README.md b/crates/integration-tests/README.md index f425ab43..c4a67fd0 100644 --- a/crates/integration-tests/README.md +++ b/crates/integration-tests/README.md @@ -1,16 +1,20 @@ # Integration Tests End-to-end tests that verify the trusted server against real frontend -containers using [Testcontainers](https://testcontainers.com/). +containers using [Testcontainers](https://testcontainers.com/) and +[Playwright](https://playwright.dev/). ## Prerequisites - **Docker** — running and accessible - **Viceroy** — Fastly local simulator (`cargo install viceroy`) - **wasm32-wasip1 target** — `rustup target add wasm32-wasip1` +- **Node.js** (LTS) — for browser tests only ## Quick start +### HTTP-level tests + ```bash ./scripts/integration-tests.sh ``` @@ -22,11 +26,29 @@ This script handles everything: 2. Builds the WordPress and Next.js Docker images 3. Runs all integration tests sequentially +### Browser tests + +```bash +./scripts/integration-tests-browser.sh +``` + +This script: + +1. Builds the WASM binary and Docker images (same as above) +2. Installs Playwright and Chromium +3. Runs browser tests for Next.js and WordPress sequentially + ### Run a single test ```bash +# HTTP-level ./scripts/integration-tests.sh test_wordpress_fastly ./scripts/integration-tests.sh test_nextjs_fastly + +# Browser — single framework +cd crates/integration-tests/browser +TEST_FRAMEWORK=nextjs npx playwright test +TEST_FRAMEWORK=wordpress npx playwright test ``` ### Verbose output @@ -59,20 +81,46 @@ docker build -t test-nextjs:latest \ ## Test scenarios -### Standard (all frameworks) +### HTTP-level — standard (all frameworks) | Scenario | What it tests | |---|---| -| `HtmlInjection` | `