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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

# env
.env*
trusted-server.toml
fastly.local.toml

# backup
**/*.rs.bk
Expand Down
8 changes: 4 additions & 4 deletions crates/integration-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,17 @@ fixtures/

1. A Docker container starts for the frontend framework, mapped to a fixed
origin port (default 8888)
2. The WASM binary is pre-built with `TRUSTED_SERVER__PUBLISHER__ORIGIN_URL`
pointing to `http://127.0.0.1:8888` so the proxy knows where to forward
2. A rendered Viceroy config projects the integration-test application config
into the local config store under the fixed `ts-config` key
3. Viceroy spawns with the WASM binary on a random port
4. **HTTP tests**: reqwest sends requests to Viceroy and asserts on responses
5. **Browser tests**: Playwright opens Chromium pointing at Viceroy and verifies
script injection, bundle loading, and client-side navigation in a real browser

### Why `--test-threads=1` / `workers: 1`

All tests share the same fixed origin port (8888). The trusted server config is
baked into the WASM binary at compile time with this port, so only one Docker
All tests share the same fixed origin port (8888). The integration-test app
config fixture points Trusted Server at this port, so only one Docker
container can be bound to it at a time.

## CI
Expand Down
42 changes: 38 additions & 4 deletions crates/integration-tests/browser/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { execFileSync } from "node:child_process";
import {
startContainer,
startViceroy,
Expand All @@ -16,14 +18,24 @@ const WASM_PATH =
"../../../target/wasm32-wasip1/release/trusted-server-adapter-fastly.wasm",
);

const VICEROY_CONFIG =
process.env.VICEROY_CONFIG_PATH ||
resolve(__dirname, "../fixtures/configs/viceroy-template.toml");
const VICEROY_TEMPLATE = resolve(
__dirname,
"../fixtures/configs/viceroy-template.toml",
);
const APP_CONFIG = resolve(
__dirname,
"../fixtures/configs/trusted-server.integration.toml",
);
const RENDER_SCRIPT = resolve(
__dirname,
"../../../scripts/render-fastly-local-config.py",
);

/** Persist current state so global-teardown can always clean up. */
function writeState(state: {
baseUrl?: string;
containerId?: string;
renderedConfigPath?: string;
viceroyPid?: number;
framework: string;
}): void {
Expand All @@ -33,6 +45,7 @@ function writeState(state: {
async function globalSetup(): Promise<void> {
const framework = process.env.TEST_FRAMEWORK || "nextjs";
let containerId: string | undefined;
let renderedConfig: string | undefined;
let viceroyPid: number | undefined;

try {
Expand All @@ -43,8 +56,22 @@ async function globalSetup(): Promise<void> {
// even if Viceroy startup fails below.
writeState({ containerId, framework });

renderedConfig = resolve(
tmpdir(),
`trusted-server-browser-${Date.now()}.toml`,
);
execFileSync("python3", [
RENDER_SCRIPT,
"--app-config",
APP_CONFIG,
"--template",
VICEROY_TEMPLATE,
"--output",
renderedConfig,
]);

console.log(`[global-setup] Starting Viceroy (WASM: ${WASM_PATH})...`);
const viceroy = await startViceroy(WASM_PATH, VICEROY_CONFIG);
const viceroy = await startViceroy(WASM_PATH, renderedConfig);
viceroyPid = viceroy.process.pid;

console.log(`[global-setup] Viceroy ready at ${viceroy.baseUrl}`);
Expand All @@ -53,6 +80,7 @@ async function globalSetup(): Promise<void> {
writeState({
baseUrl: viceroy.baseUrl,
containerId,
renderedConfigPath: renderedConfig,
viceroyPid,
framework,
});
Expand All @@ -61,6 +89,12 @@ async function globalSetup(): Promise<void> {
console.error("[global-setup] Setup failed, cleaning up...");
if (viceroyPid) await stopViceroy(viceroyPid);
if (containerId) stopContainer(containerId);
try {
const { unlinkSync } = await import("node:fs");
if (renderedConfig) unlinkSync(renderedConfig);
} catch {
// Rendered config may not exist
}

// Remove partial state file since we cleaned up manually
try {
Expand Down
14 changes: 13 additions & 1 deletion crates/integration-tests/browser/global-teardown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { stopContainer, stopViceroy } from "./helpers/infra.js";
const STATE_FILE = resolve(__dirname, ".browser-test-state.json");

async function globalTeardown(): Promise<void> {
let state: { containerId?: string; viceroyPid?: number };
let state: {
containerId?: string;
renderedConfigPath?: string;
viceroyPid?: number;
};
try {
state = JSON.parse(readFileSync(STATE_FILE, "utf-8"));
} catch {
Expand All @@ -23,6 +27,14 @@ async function globalTeardown(): Promise<void> {
stopContainer(state.containerId);
}

if (state.renderedConfigPath) {
try {
unlinkSync(state.renderedConfigPath);
} catch {
// Already removed
}
}

try {
unlinkSync(STATE_FILE);
} catch {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
[[handlers]]
path = "^/secure"
username = "user"
password = "pass"

[[handlers]]
path = "^/admin"
username = "admin"
password = "changeme"

[publisher]
domain = "test-publisher.com"
cookie_domain = ".test-publisher.com"
origin_url = "http://127.0.0.1:8888"
proxy_secret = "change-me-proxy-secret"

[edge_cookie]
secret_key = "trusted-server"

[request_signing]
enabled = false
config_store_id = "<fastly-config-store-id>"
secret_store_id = "<fastly-secret-store-id>"

[integrations.prebid]
enabled = true
server_url = "http://68.183.113.79:8000"
timeout_ms = 1000
bidders = ["kargo", "appnexus", "openx"]
debug = false
client_side_bidders = ["rubicon"]

[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-trust-server.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

[proxy]

[auction]
enabled = true
providers = ["prebid"]
timeout_ms = 2000
allowed_context_keys = ["permutive_segments"]

[integrations.aps]
enabled = false
pub_id = "your-aps-publisher-id"
endpoint = "https://origin-mocktioneer.cdintel.com/e/dtb/bid"
timeout_ms = 1000

[integrations.google_tag_manager]
enabled = false
container_id = "GTM-XXXXXX"

[integrations.adserver_mock]
enabled = false
endpoint = "https://origin-mocktioneer.cdintel.com/adserver/mediate"
timeout_ms = 1000

[integrations.adserver_mock.context_query_params]
permutive_segments = "permutive"
12 changes: 6 additions & 6 deletions crates/integration-tests/tests/common/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ pub trait RuntimeProcessHandle: Send + Sync {}

/// 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).
/// The application configuration is loaded at runtime from the platform config
/// store. Test environments render a local Viceroy/Fastly config that projects
/// a canonical TOML payload into the fixed `ts-config` key before spawning the
/// runtime.
pub trait RuntimeEnvironment: Send + Sync {
/// Platform identifier (e.g., "fastly", "cloudflare")
fn id(&self) -> &'static str;
Expand Down Expand Up @@ -112,8 +112,8 @@ pub fn wasm_binary_path() -> PathBuf {

/// 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.
/// This must match the origin URL stored in the integration-test application
/// config fixture that is projected into the local config store.
pub fn origin_port() -> u16 {
match std::env::var("INTEGRATION_ORIGIN_PORT") {
Ok(value) => value
Expand Down
65 changes: 51 additions & 14 deletions crates/integration-tests/tests/environments/fastly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ use crate::common::runtime::{
};
use error_stack::ResultExt as _;
use std::io::{BufRead as _, BufReader};
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};

/// Fastly Compute runtime using Viceroy local simulator.
///
/// 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.
/// Spawns a `viceroy` child process with the WASM binary and a rendered
/// Viceroy-specific config (KV stores, secrets, and runtime app config store
/// contents).
pub struct FastlyViceroy;

impl RuntimeEnvironment for FastlyViceroy {
Expand All @@ -22,7 +22,7 @@ impl RuntimeEnvironment for FastlyViceroy {
fn spawn(&self, wasm_path: &Path) -> TestResult<RuntimeProcess> {
let port = super::find_available_port()?;

let viceroy_config = self.viceroy_config_path();
let viceroy_config = self.render_viceroy_config()?;

let mut child = Command::new("viceroy")
.arg(wasm_path)
Expand All @@ -48,7 +48,10 @@ impl RuntimeEnvironment for FastlyViceroy {
}

// Wrap immediately so Drop::drop kills the process if readiness check fails
let handle = ViceroyHandle { child };
let handle = ViceroyHandle {
child,
rendered_config_path: viceroy_config,
};
let base_url = format!("http://127.0.0.1:{port}");

// Fastly exposes a dedicated `/health` route, so root fallback only
Expand All @@ -63,13 +66,45 @@ impl RuntimeEnvironment for FastlyViceroy {
}

impl FastlyViceroy {
/// Path to the Viceroy-specific `fastly.toml` template.
///
/// This contains `[local_server]` configuration (backends, KV stores,
/// secret stores) that Viceroy needs, separate from the application config.
fn viceroy_config_path(&self) -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("fixtures/configs/viceroy-template.toml")
/// Render a Viceroy config with the application config projected into the
/// runtime config store.
fn render_viceroy_config(&self) -> TestResult<PathBuf> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let template = manifest_dir.join("fixtures/configs/viceroy-template.toml");
let app_config = manifest_dir.join("fixtures/configs/trusted-server.integration.toml");
let unique_suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("should compute monotonic temp suffix")
.as_nanos();
let output = std::env::temp_dir().join(format!(
"trusted-server-viceroy-{unique_suffix}.toml"
));
let render_script = manifest_dir
.parent()
.and_then(Path::parent)
.expect("should find repository root")
.join("scripts/render-fastly-local-config.py");

let status = Command::new("python3")
.arg(render_script)
.arg("--app-config")
.arg(app_config)
.arg("--template")
.arg(template)
.arg("--output")
.arg(&output)
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.status()
.change_context(TestError::RuntimeSpawn)
.attach("Failed to render Viceroy config")?;

if !status.success() {
return Err(error_stack::Report::new(TestError::RuntimeSpawn)
.attach("render-fastly-local-config.py exited unsuccessfully"));
}

Ok(output)
}
}

Expand All @@ -79,6 +114,7 @@ impl FastlyViceroy {
/// preventing orphaned Viceroy processes.
struct ViceroyHandle {
child: Child,
rendered_config_path: PathBuf,
}

impl RuntimeProcessHandle for ViceroyHandle {}
Expand All @@ -87,5 +123,6 @@ impl Drop for ViceroyHandle {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let _ = std::fs::remove_file(&self.rendered_config_path);
}
}
Loading
Loading