Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ reqwest = { version = "0.12", default-features = false, features = ["blocking"]
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true

[build-dependencies]
sha2.workspace = true
51 changes: 42 additions & 9 deletions crates/e2e/build.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
use sha2::{Digest, Sha256};
use std::{env, path::Path, path::PathBuf, process::Command};

/// The legacy `assetstorage` canister shipped with dfx 0.32.0. The migration
/// plugin targets this canister, so the e2e suite deploys it (gzipped — icp-cli
/// installs `.wasm.gz` directly) rather than this repo's canister.
const LEGACY_WASM_URL: &str =
"https://github.com/dfinity/sdk/releases/download/0.32.0/assetstorage.wasm.gz";
/// sha256 published on the GH release page for `assetstorage.wasm.gz`.
const LEGACY_WASM_SHA256: &str = "04e565b3425fe7510ee16b02adcfe3f01abc9a2725c82a21cb08969241debd62";

fn main() {
println!("cargo:rerun-if-changed=../canister/src");
println!("cargo:rerun-if-changed=../canister-core/src");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=../sync-plugin/src");
println!("cargo:rerun-if-changed=../sync-core/src");

Expand All @@ -12,16 +20,12 @@ fn main() {
.parent()
.and_then(Path::parent)
.expect("crates/e2e/ must have a workspace root two levels up");
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

build_wasm(workspace_root, "canister", "wasm32-unknown-unknown");
let canister_wasm = fetch_legacy_canister(&out_dir);
build_wasm(workspace_root, "sync-plugin", "wasm32-wasip2");

println!(
"cargo:rustc-env=CANISTER_WASM={}",
workspace_root
.join("target/wasm32-unknown-unknown/release/canister.wasm")
.display()
);
println!("cargo:rustc-env=CANISTER_WASM={}", canister_wasm.display());
println!(
"cargo:rustc-env=PLUGIN_WASM={}",
workspace_root
Expand All @@ -30,6 +34,35 @@ fn main() {
);
}

/// Downloads (and caches in `OUT_DIR`) the legacy `assetstorage.wasm.gz`,
/// verifying it against the pinned sha256. Returns the path to the `.gz`.
fn fetch_legacy_canister(out_dir: &Path) -> PathBuf {
let dest = out_dir.join("assetstorage.wasm.gz");
if dest.exists() && sha256_hex(&dest) == LEGACY_WASM_SHA256 {
return dest;
}
let status = Command::new("curl")
.args(["-sSL", "-o"])
.arg(&dest)
.arg(LEGACY_WASM_URL)
.status()
.unwrap_or_else(|e| panic!("failed to spawn curl for {LEGACY_WASM_URL}: {e}"));
assert!(status.success(), "curl {LEGACY_WASM_URL} failed");

let got = sha256_hex(&dest);
assert_eq!(
got, LEGACY_WASM_SHA256,
"sha256 mismatch for assetstorage.wasm.gz (got {got}, expected {LEGACY_WASM_SHA256})"
);
dest
}

fn sha256_hex(path: &Path) -> String {
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let digest = Sha256::digest(&bytes);
digest.iter().map(|b| format!("{b:02x}")).collect()
}

fn build_wasm(workspace_root: &Path, package: &str, target: &str) {
let status = Command::new("cargo")
.args(["build", "-p", package, "--target", target, "--release"])
Expand Down
11 changes: 6 additions & 5 deletions crates/e2e/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ pub fn copy_dir_contents(src: &Path, dst: &Path) -> std::io::Result<()> {
Ok(())
}

/// Set up an isolated copy of a fixture in a temporary directory, with
/// pre-built WASM modules placed at `wasms/canister.wasm` and
/// `wasms/plugin.wasm` (paths supplied by the build script).
/// Set up an isolated copy of a fixture in a temporary directory, with the
/// pre-built modules placed at `wasms/canister.wasm.gz` (the legacy dfx 0.32.0
/// `assetstorage`, installed gzipped) and `wasms/plugin.wasm` (paths supplied
/// by the build script).
///
/// `fixture_path` is relative to the e2e crate root (e.g. `"tests/fixture/basic"`).
/// The returned `TempDir` must be kept alive for the duration of the test.
Expand All @@ -97,8 +98,8 @@ pub fn setup_project(fixture_path: &str) -> tempfile::TempDir {
let wasms_dir = tmp.path().join("wasms");
fs::create_dir_all(&wasms_dir).expect("failed to create wasms/ dir");

fs::copy(env!("CANISTER_WASM"), wasms_dir.join("canister.wasm"))
.expect("failed to copy canister.wasm");
fs::copy(env!("CANISTER_WASM"), wasms_dir.join("canister.wasm.gz"))
.expect("failed to copy canister.wasm.gz");
fs::copy(env!("PLUGIN_WASM"), wasms_dir.join("plugin.wasm"))
.expect("failed to copy plugin.wasm");

Expand Down
70 changes: 70 additions & 0 deletions crates/e2e/tests/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! End-to-end checks for `.ic-assets.json5` handling against the legacy
//! dfx 0.32.0 `assetstorage` canister: per-asset headers, `cache.max_age`,
//! `enable_aliasing`, and the `security_policy` CSP preset are all applied, and
//! the config file itself is never uploaded as an asset.

use e2e::{get_asset_properties, http_fetch, icp_cmd, list_assets, setup_project, LocalNetwork};

#[test]
fn config_applies_properties_headers_and_aliasing() {
let tmp = setup_project("tests/fixture/config");
let project = tmp.path();
let _network = LocalNetwork::start(project);

icp_cmd(project).arg("deploy").assert().success();

// The `.ic-assets.json5` file is configuration, not an asset.
let assets = list_assets(project);
assert!(
!assets.iter().any(|a| a.key == "/.ic-assets.json5"),
"the config file must not be uploaded as an asset; got: {assets:#?}",
);
assert!(
assets.iter().any(|a| a.key == "/index.html"),
"expected /index.html in asset list; got: {assets:#?}",
);

// index.html resolves both rules: max_age 600 + aliasing from the *.html
// rule, plus the standard security policy + custom header from **/*.
let props = get_asset_properties(project, "/index.html");
assert_eq!(props.max_age, Some(600), "max_age from cache config");
assert_eq!(props.is_aliased, Some(true), "enable_aliasing from config");

let headers = props.headers.expect("index.html should have headers");
let has = |name: &str| headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
assert!(
headers
.iter()
.any(|(k, v)| k.eq_ignore_ascii_case("X-Custom") && v == "yes"),
"custom header from config missing; got: {headers:#?}",
);
assert!(
has("Content-Security-Policy"),
"standard security policy CSP header missing; got: {headers:#?}",
);

// style.css gets only the **/* rule: custom header + CSP, but no max_age.
let css_props = get_asset_properties(project, "/style.css");
assert_eq!(css_props.max_age, None, "css has no cache rule");
let css_headers = css_props.headers.unwrap_or_default();
assert!(
css_headers
.iter()
.any(|(k, _)| k.eq_ignore_ascii_case("X-Custom")),
"css should still carry the **/* custom header; got: {css_headers:#?}",
);

// The certified HTTP response carries the custom header (going through the
// gateway implicitly validates certification).
let resp = http_fetch(project, "/index.html");
assert!(
resp.status().is_success(),
"GET /index.html: {}",
resp.status()
);
assert_eq!(
resp.headers().get("x-custom").and_then(|v| v.to_str().ok()),
Some("yes"),
"custom header missing from HTTP response",
);
}
2 changes: 1 addition & 1 deletion crates/e2e/tests/fixture/basic/icp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ canisters:
build:
steps:
- type: pre-built
path: wasms/canister.wasm
path: wasms/canister.wasm.gz

sync:
steps:
Expand Down
16 changes: 16 additions & 0 deletions crates/e2e/tests/fixture/config/dist/.ic-assets.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
// Applies a standard security policy and a custom header to every asset.
{
"match": "**/*",
"security_policy": "standard",
"headers": {
"X-Custom": "yes",
},
},
// HTML files additionally get a cache max-age and explicit aliasing.
{
"match": "*.html",
"cache": { "max_age": 600 },
"enable_aliasing": true,
},
]
5 changes: 5 additions & 0 deletions crates/e2e/tests/fixture/config/dist/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!DOCTYPE html>
<html>
<head><title>config fixture</title></head>
<body><h1>config fixture index</h1></body>
</html>
1 change: 1 addition & 0 deletions crates/e2e/tests/fixture/config/dist/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
body { font-family: sans-serif; }
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ canisters:
build:
steps:
- type: pre-built
path: wasms/canister.wasm
path: wasms/canister.wasm.gz

sync:
steps:
Expand Down
4 changes: 0 additions & 4 deletions crates/e2e/tests/fixture/headers-content-type/dist/_headers

This file was deleted.

3 changes: 0 additions & 3 deletions crates/e2e/tests/fixture/headers-content-type/dist/ic.did

This file was deleted.

This file was deleted.

19 changes: 0 additions & 19 deletions crates/e2e/tests/fixture/headers-content-type/icp.yaml

This file was deleted.

1 change: 0 additions & 1 deletion crates/e2e/tests/fixture/headers/dist/_astro/app.js

This file was deleted.

10 changes: 0 additions & 10 deletions crates/e2e/tests/fixture/headers/dist/_headers

This file was deleted.

1 change: 0 additions & 1 deletion crates/e2e/tests/fixture/headers/dist/index.html

This file was deleted.

19 changes: 0 additions & 19 deletions crates/e2e/tests/fixture/headers/icp.yaml

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

19 changes: 0 additions & 19 deletions crates/e2e/tests/fixture/html-handling-with-catchall/icp.yaml

This file was deleted.

This file was deleted.

1 change: 0 additions & 1 deletion crates/e2e/tests/fixture/html-handling/dist/foo.html

This file was deleted.

1 change: 0 additions & 1 deletion crates/e2e/tests/fixture/html-handling/dist/index.html

This file was deleted.

5 changes: 0 additions & 5 deletions crates/e2e/tests/fixture/multi-dir/dist-a/page.html

This file was deleted.

1 change: 0 additions & 1 deletion crates/e2e/tests/fixture/multi-dir/dist-b/app.js

This file was deleted.

Loading
Loading