#![cfg(all(target_arch = "wasm32", feature = "workers"))]
use cachekit::session::session_headers;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test]
fn session_headers_valid_on_wasm32() {
let headers = session_headers();
assert_eq!(headers[0].0, "X-CacheKit-Session-ID");
assert_eq!(headers[1].0, "X-CacheKit-Session-Start");
let id = uuid::Uuid::parse_str(headers[0].1).expect("session ID should be a valid UUID");
assert_eq!(id.get_version_num(), 4, "should be UUID v4");
let start: u64 = headers[1]
.1
.parse()
.expect("session start should be numeric");
assert!(
start > 1_704_067_200_000,
"start {start} should be after 2024"
);
assert!(
start < 4_102_444_800_000,
"start {start} should be before 2100"
);
}
#[wasm_bindgen_test]
async fn workers_backend_get_reaches_fetch_without_trapping() {
use cachekit::backend::workers::WorkersCachekitIO;
use cachekit::backend::Backend;
let backend = WorkersCachekitIO::builder()
.api_key("test-key-never-sent")
.api_url("https://cachekit-wasm-clock.invalid")
.allow_custom_host(true)
.build()
.expect("builder should accept a syntactically valid https URL");
let result = backend.get("wasm-clock-regression-key").await;
assert!(
result.is_err(),
".invalid must not resolve — expected a network error, got {result:?}"
);
}
const REDIRECT_SERVER_JS: &str = r"
const http = process.getBuiltinModule('node:http');
const state = (globalThis.__ckRedirect = { targetHits: 0 });
const server = http.createServer((req, res) => {
req.resume();
const status = /^\/v1\/cache\/redirect-(3\d\d)$/.exec(req.url)?.[1];
if (status) return res.writeHead(Number(status), { location: '/v1/cache/redirected' }).end();
if (req.url === '/v1/cache/redirected') {
state.targetHits++;
return res.writeHead(200).end('followed');
}
res.writeHead(404).end();
});
server.unref();
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => {
const origin = `http://127.0.0.1:${server.address().port}`;
const realFetch = globalThis.fetch;
globalThis.fetch = async (input, init) => {
const req = new Request(input, init);
const body = ['GET', 'HEAD'].includes(req.method) ? undefined : await req.arrayBuffer();
return realFetch(origin + new URL(req.url).pathname, {
method: req.method, headers: req.headers, body, redirect: req.redirect,
});
};
state.close = () => {
globalThis.fetch = realFetch;
server.closeAllConnections();
server.close();
};
resolve();
}));
";
#[wasm_bindgen_test]
async fn workers_backend_does_not_follow_redirects() {
use cachekit::backend::workers::WorkersCachekitIO;
use cachekit::backend::Backend;
use cachekit::BackendErrorKind;
use worker::js_sys::{global, Function, Promise, Reflect};
use worker::wasm_bindgen::{JsCast, JsValue};
use worker::wasm_bindgen_futures::JsFuture;
let backend = WorkersCachekitIO::builder()
.api_key("test-key-never-sent")
.build()
.expect("default URL is valid");
let start: Promise = Function::new_no_args(REDIRECT_SERVER_JS)
.call0(&JsValue::NULL)
.expect("start redirect server")
.unchecked_into();
JsFuture::from(start)
.await
.expect("redirect server listening");
let state = Reflect::get(&global(), &"__ckRedirect".into()).expect("server state");
let target_hits = || {
Reflect::get(&state, &"targetHits".into())
.ok()
.and_then(|v| v.as_f64())
};
let mut init = worker::RequestInit::new();
init.with_redirect(worker::RequestRedirect::Follow);
let control = match worker::Request::new_with_init(
"https://api.cachekit.io/v1/cache/redirect-302",
&init,
) {
Ok(request) => worker::Fetch::Request(request)
.send()
.await
.map(|resp| resp.status_code()),
Err(e) => Err(e),
};
let control_hits = target_hits();
let _ = Reflect::set(&state, &"targetHits".into(), &0.into());
let mut results = Vec::new();
for status in [301, 302, 303, 307, 308] {
let key = format!("redirect-{status}");
results.push(("get", status, backend.get(&key).await.err()));
results.push(("set", status, backend.set(&key, vec![1], None).await.err()));
results.push(("delete", status, backend.delete(&key).await.err()));
results.push(("exists", status, backend.exists(&key).await.err()));
}
let hits = target_hits();
if let Ok(close) = Reflect::get(&state, &"close".into()) {
let _ = close.unchecked_into::<Function>().call0(&JsValue::NULL);
}
assert_eq!(control.ok(), Some(200), "control: the harness follows");
assert_eq!(
control_hits,
Some(1.0),
"control: the target saw the follow"
);
for (op, status, err) in results {
let err = err.unwrap_or_else(|| panic!("{op} on HTTP {status} must be an error"));
assert_eq!(
err.kind,
BackendErrorKind::Permanent,
"{op} on HTTP {status}: {err}"
);
}
assert_eq!(hits, Some(0.0), "a redirect was followed");
}
#[wasm_bindgen_test]
fn workers_backend_uses_the_validated_url_as_serialized() {
use cachekit::backend::workers::WorkersCachekitIO;
let backend = WorkersCachekitIO::builder()
.api_key("test-key-never-sent")
.api_url("https://api.cachekit.io\\@evil.example")
.build()
.expect("allowlisted host");
assert_eq!(backend.api_url(), "https://api.cachekit.io/@evil.example");
}
const STALL_FETCH_JS: &str = r"
const state = (globalThis.__ckStall = { signals: [] });
const realFetch = globalThis.fetch;
globalThis.fetch = (input, init) => {
state.signals.push(init?.signal);
const path = new URL(new Request(input).url).pathname;
if (path === '/v1/cache/stall-body') {
return Promise.resolve(new Response(new ReadableStream({}), { status: 200 }));
}
return new Promise(() => {});
};
state.close = () => { globalThis.fetch = realFetch; };
";
async fn join_timed<A: std::future::Future, B: std::future::Future>(
a: A,
b: B,
) -> ((A::Output, f64), (B::Output, f64)) {
use std::task::Poll;
use worker::js_sys::Date;
let (mut a, mut b) = (std::pin::pin!(a), std::pin::pin!(b));
let (mut done_a, mut done_b) = (None, None);
std::future::poll_fn(|cx| {
if done_a.is_none() {
if let Poll::Ready(out) = a.as_mut().poll(cx) {
done_a = Some((out, Date::now()));
}
}
if done_b.is_none() {
if let Poll::Ready(out) = b.as_mut().poll(cx) {
done_b = Some((out, Date::now()));
}
}
if done_a.is_some() && done_b.is_some() {
Poll::Ready(())
} else {
Poll::Pending
}
})
.await;
(done_a.expect("a finished"), done_b.expect("b finished"))
}
#[wasm_bindgen_test]
async fn workers_backend_times_out_a_stalled_request() {
use cachekit::backend::workers::WorkersCachekitIO;
use cachekit::backend::Backend;
use cachekit::BackendErrorKind;
use worker::js_sys::{global, Array, Date, Function, Reflect};
use worker::wasm_bindgen::{JsCast, JsValue};
let backend = WorkersCachekitIO::builder()
.api_key("test-key-never-sent")
.build()
.expect("default URL is valid");
Function::new_no_args(STALL_FETCH_JS)
.call0(&JsValue::NULL)
.expect("patch fetch");
let state = Reflect::get(&global(), &"__ckStall".into()).expect("stall state");
let start = Date::now();
let ((get_hang, get_hang_at), (((get_body, get_body_at), (set_hang, set_hang_at)), _)) =
join_timed(
backend.get("hang"),
join_timed(
backend.get("stall-body"),
backend.set("hang", vec![1], None),
),
)
.await;
let signals: Array = Reflect::get(&state, &"signals".into())
.expect("signals")
.unchecked_into();
let aborted: Vec<bool> = signals
.iter()
.map(|s| {
Reflect::get(&s, &"aborted".into())
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(false)
})
.collect();
if let Ok(close) = Reflect::get(&state, &"close".into()) {
let _ = close.unchecked_into::<Function>().call0(&JsValue::NULL);
}
let calls = [
("get, no response", get_hang.err(), get_hang_at, 5_000.0),
("get, stalled body", get_body.err(), get_body_at, 5_000.0),
("set, no response", set_hang.err(), set_hang_at, 10_000.0),
];
for (call, err, at, budget) in calls {
let err = err.unwrap_or_else(|| panic!("{call}: must fail"));
assert_eq!(err.kind, BackendErrorKind::Timeout, "{call}: {err}");
let elapsed = at - start;
assert!(
elapsed >= budget - 50.0 && elapsed <= budget + 1_000.0,
"{call}: gave up after {elapsed} ms, budget {budget} ms"
);
}
assert_eq!(
aborted,
[true, true, true],
"every stalled request is aborted"
);
}
async fn sleep_ms(ms: u32) {
use worker::js_sys::{Function, Promise};
use worker::wasm_bindgen::{JsCast, JsValue};
use worker::wasm_bindgen_futures::JsFuture;
let wait: Promise =
Function::new_with_args("ms", "return new Promise(r => setTimeout(r, ms));")
.call1(&JsValue::NULL, &ms.into())
.expect("sleep")
.unchecked_into();
JsFuture::from(wait).await.expect("sleep resolves");
}
#[wasm_bindgen_test]
async fn workers_backend_frees_the_deadline_of_a_completed_request() {
use cachekit::backend::workers::WorkersCachekitIO;
use cachekit::backend::Backend;
use worker::js_sys::Function;
use worker::wasm_bindgen::{externref_heap_live_count, JsCast, JsValue};
let restore: Function = Function::new_no_args(
"const old = globalThis.fetch;
globalThis.fetch = () => Promise.resolve(new Response('ok'));
return () => { globalThis.fetch = old; };",
)
.call0(&JsValue::NULL)
.expect("patch fetch")
.unchecked_into();
let backend = WorkersCachekitIO::builder()
.api_key("test-key-never-sent")
.build()
.expect("default URL is valid");
let warmup = backend.get("warmup").await;
sleep_ms(0).await;
let before = externref_heap_live_count();
let mut results = Vec::new();
for _ in 0..200 {
results.push(backend.get("ok").await);
}
sleep_ms(0).await;
let retained = externref_heap_live_count() - before;
let _ = restore.call0(&JsValue::NULL);
assert_eq!(warmup.ok(), Some(Some(b"ok".to_vec())));
assert!(results
.iter()
.all(|r| r.as_ref().ok() == Some(&Some(b"ok".to_vec()))));
assert!(
retained < 10,
"200 completed requests retained {retained} live JS references"
);
}
#[wasm_bindgen_test]
async fn workers_backend_aborts_a_dropped_request() {
use cachekit::backend::workers::WorkersCachekitIO;
use cachekit::backend::Backend;
use std::task::Poll;
use worker::js_sys::{global, Function, Reflect};
use worker::wasm_bindgen::{JsCast, JsValue};
let restore: Function = Function::new_no_args(
"const old = globalThis.fetch;
globalThis.fetch = (_input, init) => {
globalThis.__ckDropped = init.signal;
return new Promise((_resolve, reject) =>
init.signal.addEventListener('abort', () => reject(new Error('aborted'))));
};
return () => { globalThis.fetch = old; };",
)
.call0(&JsValue::NULL)
.expect("patch fetch")
.unchecked_into();
let backend = WorkersCachekitIO::builder()
.api_key("test-key-never-sent")
.build()
.expect("default URL is valid");
let mut call = backend.get("dropped");
let pending = std::future::poll_fn(|cx| Poll::Ready(call.as_mut().poll(cx).is_pending())).await;
drop(call);
sleep_ms(0).await;
let aborted = Reflect::get(&global(), &"__ckDropped".into())
.and_then(|signal| Reflect::get(&signal, &"aborted".into()))
.ok()
.and_then(|v| v.as_bool());
let _ = restore.call0(&JsValue::NULL);
assert!(pending, "the call was in flight when dropped");
assert_eq!(aborted, Some(true), "dropping the call aborts its request");
}