adversarial.rs•33.8 kB
use std::{
str::FromStr,
sync::LazyLock,
};
use common::{
assert_obj,
bootstrap_model::index::{
database_index::IndexedFields,
IndexMetadata,
},
components::{
CanonicalizedComponentFunctionPath,
ComponentPath,
PublicFunctionPath,
},
knobs::ISOLATE_MAX_ARRAY_BUFFER_TOTAL_SIZE,
log_lines::{
LogLines,
TRUNCATED_LINE_SUFFIX,
},
testing::assert_contains,
types::{
AllowedVisibility,
UdfType,
},
value::{
ConvexArray,
ConvexValue,
},
version::Version,
};
use database::SystemMetadataModel;
use keybroker::Identity;
use model::udf_config::UdfConfigModel;
use must_let::must_let;
use runtime::{
prod::ProdRuntime,
testing::TestRuntime,
};
use sync_types::CanonicalizedUdfPath;
use udf::validation::ValidatedPathAndArgs;
use value::{
assert_val,
ConvexBytes,
TableNamespace,
};
use crate::{
concurrency_limiter::ConcurrencyLimiter,
environment::helpers::MAX_LOG_LINES,
test_helpers::{
UdfTest,
UdfTestConfig,
DEFAULT_CONFIG,
},
tests::logging::nested_function_udf_test,
IsolateConfig,
};
static MAX_ISOLATE_WORKERS: usize = 1;
static TIMEOUT_CONFIG: LazyLock<UdfTestConfig> = LazyLock::new(|| UdfTestConfig {
isolate_config: IsolateConfig::new("timeout_test", ConcurrencyLimiter::unlimited()),
udf_server_version: Version::parse("1000.0.0").unwrap(),
});
#[convex_macro::prod_rt_test]
async fn test_time_out(rt: ProdRuntime) -> anyhow::Result<()> {
let t = UdfTest::default_with_config(TIMEOUT_CONFIG.clone(), MAX_ISOLATE_WORKERS, rt.clone())
.await?;
let e = t
.query_js_error("adversarial:simpleLoop", assert_obj!())
.await?;
assert_contains(&e, "Function execution timed out");
Ok(())
}
#[convex_macro::prod_rt_test]
async fn test_wasm_simple_loop(rt: ProdRuntime) -> anyhow::Result<()> {
let t = UdfTest::default_with_config(TIMEOUT_CONFIG.clone(), MAX_ISOLATE_WORKERS, rt.clone())
.await?;
let e = t
.query_js_error("adversarialWasm:simpleLoop", assert_obj!())
.await?;
assert_contains(&e, "Function execution timed out");
Ok(())
}
#[convex_macro::prod_rt_test]
async fn test_wasm_allocating_loop(rt: ProdRuntime) -> anyhow::Result<()> {
let t = UdfTest::default_with_config(TIMEOUT_CONFIG.clone(), MAX_ISOLATE_WORKERS, rt.clone())
.await?;
let e = t
.query_js_error("adversarialWasm:allocatingLoop", assert_obj!())
.await?;
assert_contains(&e, "Function execution timed out");
Ok(())
}
// We don't allow setTimeout in queries, so it's not possible to get
// a query to execute for a precise amount of time. This makes this
// test flaky:
#[ignore]
#[convex_macro::prod_rt_test]
async fn test_almost_time_out(rt: ProdRuntime) -> anyhow::Result<()> {
let t = UdfTest::default_with_config(TIMEOUT_CONFIG.clone(), MAX_ISOLATE_WORKERS, rt.clone())
.await?;
let mut log_lines = t.query_log_lines("adversarial:slow", assert_obj!()).await?;
assert_contains(
&log_lines.pop().unwrap().to_pretty_string_test_only(),
"[WARN] Function execution took a long time",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_db_loop(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
t.mutation("adversarial:populate", assert_obj!()).await?;
let e = t
.query_js_error("adversarial:dbLoop", assert_obj!())
.await?;
assert_contains(&e, "Too many documents read");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_read_too_many_documents(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
t.mutation("adversarial:populate", assert_obj!()).await?;
let e = t
.query_js_error("adversarial:queryLeak", assert_obj!())
.await?;
assert_contains(&e, "Too many documents read in a single function execution");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_read_many_documents(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
t.mutation("adversarial:populate", assert_obj!()).await?;
let mut log_lines = t
.query_log_lines("adversarial:queryATon", assert_obj!())
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Many documents read in a single function execution",
);
assert_contains(&last_line, "Consider using smaller limits in your queries");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_reads_too_many(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
t.add_index(IndexMetadata::new_backfilling(
*t.database.now_ts_for_reads(),
"test.by_hello".parse()?,
IndexedFields::try_from(vec!["hello".parse()?])?,
))
.await?;
t.backfill_indexes().await?;
let e = t
.query_js_error("adversarial:queryTooManyTimes", assert_obj!())
.await?;
assert_contains(&e, "Too many reads in a single function execution");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_reads_many(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
t.add_index(IndexMetadata::new_backfilling(
*t.database.now_ts_for_reads(),
"test.by_hello".parse()?,
IndexedFields::try_from(vec!["hello".parse()?])?,
))
.await?;
t.backfill_indexes().await?;
let mut log_lines = t
.query_log_lines("adversarial:queryManyTimes", assert_obj!())
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Many reads in a single function execution",
);
assert_contains(&last_line, "Consider using smaller limits in your queries");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_console_loop(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let mut log_lines = t
.query_log_lines("adversarial:consoleLoop", assert_obj!())
.await?;
assert_eq!(log_lines.len(), MAX_LOG_LINES);
assert_contains(
&log_lines.pop().unwrap().to_pretty_string_test_only(),
"Log overflow",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_console_loop_from_subfunction(rt: TestRuntime) -> anyhow::Result<()> {
let t = nested_function_udf_test(rt).await?;
let log_lines = t
.query_log_lines("adversarial:consoleLoopFromSubfunction", assert_obj!())
.await?;
let log_lines_flat: Vec<_> = log_lines
.into_iter()
.flat_map(|log_line| log_line.to_pretty_strings())
.collect();
assert_eq!(log_lines_flat.len(), 256);
let mut expected_log_lines = vec![];
// From child.
expected_log_lines.extend(["[LOG] 'are we there yet'"].repeat(200).into_iter());
// From parent.
expected_log_lines.push("[LOG] 'we get there when we get there'");
// From child, truncated.
expected_log_lines.extend(["[LOG] 'are we there yet'"].repeat(54).into_iter());
expected_log_lines.push("[ERROR] Log overflow (maximum 256). Remaining log lines omitted.");
assert_eq!(
log_lines_flat,
expected_log_lines
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_console_line_too_long(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let log_lines = t
.query_log_lines("adversarial:consoleLongLine", assert_obj!())
.await?;
assert_contains(
&log_lines[0].clone().to_pretty_string_test_only(),
TRUNCATED_LINE_SUFFIX,
);
// The limit is 32768 MAX_LOG_LINE_LENGTH, but we don't count the [INFO]
// prefix or the truncated line suffix, so just check that we're close
assert!(log_lines[0].clone().to_pretty_string_test_only().len() > 32700);
assert!(log_lines[0].clone().to_pretty_string_test_only().len() < 32900);
assert_contains(
&log_lines[1].clone().to_pretty_string_test_only(),
TRUNCATED_LINE_SUFFIX,
);
// The limit is 32768 MAX_LOG_LINE_LENGTH, but we don't count the [INFO]
// prefix or the truncated line suffix, so just check that we're close
assert!(log_lines[1].clone().to_pretty_string_test_only().len() > 32700);
assert!(log_lines[1].clone().to_pretty_string_test_only().len() < 32900);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_console_line_too_long_char_boundary(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let log_lines = t
.query_log_lines("adversarial:consoleLongLineCharBoundary", assert_obj!())
.await?;
assert_eq!(log_lines.len(), 4);
assert_contains(
&log_lines[0].clone().to_pretty_string_test_only(),
TRUNCATED_LINE_SUFFIX,
);
assert_contains(
&log_lines[1].clone().to_pretty_string_test_only(),
TRUNCATED_LINE_SUFFIX,
);
assert_contains(
&log_lines[2].clone().to_pretty_string_test_only(),
TRUNCATED_LINE_SUFFIX,
);
assert_contains(
&log_lines[3].clone().to_pretty_string_test_only(),
TRUNCATED_LINE_SUFFIX,
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_unsupported_apis(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
t.query("adversarial:tryUnsupportedAPIs", assert_obj!())
.await?;
Ok(())
}
#[convex_macro::test_runtime]
async fn test_big_read(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let ids = t
.mutation("adversarial:populateBigRead", assert_obj!())
.await?;
let e = t
.query_js_error("adversarial:bigRead", assert_obj!("ids" => ids))
.await?;
assert_contains(&e, "Too many reads");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_big_return(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_js_error("adversarial:returnTooLarge", assert_obj!())
.await?;
assert_contains(&e, "Array length is too long");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_iterate_twice(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_js_error("adversarial:iterateTwice", assert_obj!())
.await?;
assert_contains(&e, "This query is closed and can't emit any more values.");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_iterate_consumed(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_js_error("adversarial:iterateConsumed", assert_obj!())
.await?;
assert_contains(
&e,
"This query has been chained with another operator and can't be reused.",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_reads_too_large(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
// 32 documents per write * 256KiB per document * 5 writes = 40 MiB, which is
// higher that the limit on reads.
let count_per_write = 32.0;
let mut ids: Vec<ConvexValue> = vec![];
for _ in 0..5 {
let more_ids = t
.mutation(
"adversarial:bigWrite",
assert_obj!("count" => count_per_write),
)
.await?;
must_let!(let ConvexValue::Array(more_ids) = more_ids);
ids.extend(Vec::<ConvexValue>::from(more_ids));
}
let e = t
.query_js_error("adversarial:bigRead", assert_obj!("ids" => ids.clone()))
.await?;
assert_contains(&e, "Too many bytes read in a single function execution");
let ret_val = t.query("adversarial:readUntilError", assert_obj!()).await?;
must_let!(let ConvexValue::Array(ret_val) = ret_val);
assert!(!ret_val.is_empty());
assert!(ret_val.len() < ids.len());
Ok(())
}
#[convex_macro::test_runtime]
async fn test_reads_large(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
// 12 documents per write * 256KiB per document * 5 writes = 14 MiB, which is
// close to the limit on reads.
let count_per_write = 12.0;
let mut ids: Vec<ConvexValue> = vec![];
for _ in 0..5 {
let more_ids = t
.mutation(
"adversarial:bigWrite",
assert_obj!("count" => count_per_write),
)
.await?;
must_let!(let ConvexValue::Array(more_ids) = more_ids);
ids.extend(Vec::<ConvexValue>::from(more_ids));
}
let mut log_lines = t
.query_log_lines("adversarial:bigRead", assert_obj!("ids" => ids.clone()))
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Many bytes read in a single function execution",
);
assert_contains(&last_line, "Consider using smaller limits in your queries");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_writes_too_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let count = 64.0;
let e = t
.mutation_js_error("adversarial:bigWrite", assert_obj!("count" => count))
.await?;
assert_contains(&e, "Too many bytes written in a single function execution");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_writes_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let count = 60.0;
let mut log_lines = t
.mutation_log_lines("adversarial:bigWrite", assert_obj!("count" => count))
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Many bytes written in a single function execution",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_writes_big_document(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let (id, outcome) = t
.mutation_outcome("adversarial:bigDocument", assert_obj!(), Identity::system())
.await?;
let last_line = outcome
.log_lines
.last()
.unwrap()
.clone()
.to_pretty_string_test_only();
assert_contains(
&last_line,
&format!("[WARN] Large document written with ID {id}"),
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_writes_nested_document(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let (id, outcome) = t
.mutation_outcome(
"adversarial:nestedDocument",
assert_obj!(),
Identity::system(),
)
.await?;
let last_line = outcome
.log_lines
.last()
.unwrap()
.clone()
.to_pretty_string_test_only();
assert_contains(
&last_line,
&format!("[WARN] Deeply nested document written with ID {id}"),
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_oom(rt: TestRuntime) -> anyhow::Result<()> {
UdfTest::run_test_with_isolate2(rt, async move |t| {
let e = t.query_js_error("adversarial:oom", assert_obj!()).await?;
assert_contains(&e, "JavaScript execution ran out of memory");
Ok(())
})
.await
}
#[convex_macro::test_runtime]
async fn test_writes_too_many(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.mutation_js_error("adversarial:tooManyWrites", assert_obj!())
.await?;
assert_contains(&e, "Too many writes in a single function execution");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_writes_many(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let mut log_lines = t
.mutation_log_lines("adversarial:manyWrites", assert_obj!())
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Many writes in a single function execution",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_query_args_too_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let data: ConvexValue = ConvexValue::Bytes(ConvexBytes::try_from(vec![0; 17_000_000])?);
let e = t
.query_js_error("basic:readTime", assert_obj!("data" => data))
.await?;
assert_contains(
&e,
"Arguments for basic.js:readTime are too large (actual: 16.21 MiB, limit: 16 MiB)",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_mutation_args_too_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let data: ConvexValue = ConvexValue::Bytes(ConvexBytes::try_from(vec![0; 17_000_000])?);
let e = t
.mutation_js_error("basic:simpleMutation", assert_obj!("data" => data))
.await?;
assert_contains(
&e,
"Arguments for basic.js:simpleMutation are too large (actual: 16.21 MiB, limit: 16 MiB)",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_action_args_too_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let data: ConvexValue = ConvexValue::Bytes(ConvexBytes::try_from(vec![0; 17_000_000])?);
let e = t
.action_js_error("basic:simpleAction", assert_obj!("data" => data))
.await?;
assert_contains(
&e,
"Arguments for basic.js:simpleAction are too large (actual: 16.21 MiB, limit: 16 MiB)",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_query_args_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let data: ConvexValue = ConvexValue::Bytes(ConvexBytes::try_from(vec![0; 16_000_000])?);
let mut log_lines = t
.query_log_lines("basic:readTime", assert_obj!("data" => data))
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Large size of the function arguments (actual: 16000011 bytes, limit: 16777216 \
bytes).",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_mutation_args_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let data: ConvexValue = ConvexValue::Bytes(ConvexBytes::try_from(vec![0; 16_000_000])?);
let mut log_lines = t
.mutation_log_lines("basic:simpleMutation", assert_obj!("data" => data))
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Large size of the function arguments (actual: 16000011 bytes, limit: 16777216 \
bytes).",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_action_args_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let data: ConvexValue = ConvexValue::Bytes(ConvexBytes::try_from(vec![0; 16_000_000])?);
let mut log_lines = t
.action_log_lines("basic:simpleAction", assert_obj!("data" => data))
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Large size of the action arguments (actual: 16000011 bytes, limit: 16777216 \
bytes).",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_query_result_too_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_js_error(
"adversarial:queryResultSized",
assert_obj!("size" => 17_000_000.0),
)
.await?;
assert_contains(
&e,
"Function adversarial.js:queryResultSized return value is too large (actual: 16.21 MiB, \
limit: 16 MiB)",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_mutation_result_too_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.mutation_js_error(
"adversarial:mutationResultSized",
assert_obj!("size" => 17_000_000.0),
)
.await?;
assert_contains(
&e,
"Function adversarial.js:mutationResultSized return value is too large (actual: 16.21 \
MiB, limit: 16 MiB)",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_action_result_too_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.action_js_error(
"adversarial:actionResultSized",
assert_obj!("size" => 17_000_000.0),
)
.await?;
assert_contains(
&e,
"Function adversarial.js:actionResultSized return value is too large (actual: 16.21 MiB, \
limit: 16 MiB)",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_query_result_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let mut log_lines = t
.query_log_lines(
"adversarial:queryResultSized",
assert_obj!("size" => 16_000_000.0),
)
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Large size of the function return value (actual: 16000002 bytes, limit: 16777216 \
bytes).",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_mutation_result_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let mut log_lines = t
.mutation_log_lines(
"adversarial:mutationResultSized",
assert_obj!("size" => 16_000_000.0),
)
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Large size of the function return value (actual: 16000002 bytes, limit: 16777216 \
bytes).",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_action_result_big(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let mut log_lines = t
.action_log_lines(
"adversarial:actionResultSized",
assert_obj!("size" => 16_000_000.0),
)
.await?;
let last_line = log_lines.pop().unwrap().to_pretty_string_test_only();
assert_contains(
&last_line,
"[WARN] Large size of the action return value (actual: 16000002 bytes, limit: 16777216 \
bytes).",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_no_eval(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
// TODO: Reenable this test.
// let e = t.transaction("adversarial:tryEval", assert_obj!()).unwrap_err();
// assert!(format!("{e}").contains("Code generation from strings disallowed for
// this context"));
let e = t
.mutation_js_error("adversarial:tryNewFunction", assert_obj!())
.await?;
assert_contains(
&e,
"Code generation from strings disallowed for this context",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_delete_convex_global(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_js_error("adversarial:deleteConvexGlobal", assert_obj!())
.await?;
assert_contains(
&e,
"The Convex database and auth objects are being used outside of a Convex backend.",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_throw_system_error(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_outcome(
"adversarial:throwSystemError",
assert_obj!(),
Identity::system(),
)
.await
.unwrap_err();
assert_contains(&e, "I can't go for that");
// Check that system errors work after an `.await` -- the code after the `await`
// runs in the microtask queue rather than the direct call stack entered by
// `Isolate::run`.
let e = t
.query_outcome(
"adversarial:throwSystemErrorAfterAwait",
assert_obj!(),
Identity::system(),
)
.await
.unwrap_err();
assert_contains(&e, "I can't go for that");
Ok(())
}
#[convex_macro::prod_rt_test]
async fn test_slow_syscall(rt: ProdRuntime) -> anyhow::Result<()> {
let t = UdfTest::default_with_config(TIMEOUT_CONFIG.clone(), MAX_ISOLATE_WORKERS, rt.clone())
.await?;
let r = t.query("adversarial:slowSyscall", assert_obj!()).await?;
assert_eq!(r, assert_val!(1017.));
Ok(())
}
#[convex_macro::prod_rt_test]
#[ignore]
async fn test_really_slow_syscall(rt: ProdRuntime) -> anyhow::Result<()> {
let t = UdfTest::default_with_config(TIMEOUT_CONFIG.clone(), MAX_ISOLATE_WORKERS, rt.clone())
.await?;
let e = t
.query("adversarial:reallySlowSyscall", assert_obj!())
.await
.unwrap_err();
assert_contains(&e, "Hit maximum total syscall duration");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_path_within_deps(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_js_error_no_validation("_deps/nonexistent", assert_obj!())
.await
.unwrap_err();
assert_contains(
&e,
"Refusing to run _deps/nonexistent.js:default within the '_deps' directory",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_udf_type_mismatch(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_js_error("adversarial:populate", assert_obj!())
.await?;
assert_contains(
&e,
"Trying to execute adversarial.js:populate as Query, but it is defined as Mutation.",
);
let e = t
.mutation_js_error("adversarial:simpleLoop", assert_obj!())
.await?;
assert_contains(
&e,
"Trying to execute adversarial.js:simpleLoop as Mutation, but it is defined as Query.",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_create_system_field(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.mutation_js_error("basic:insertObject", assert_obj!("_systemField" => 0))
.await?;
assert_contains(
&e,
"Field '_systemField' starts with an underscore, which is only allowed for system fields \
like '_id'",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_atomics_wait(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_js_error("adversarial:atomicsWait", assert_obj!())
.await?;
assert_contains(&e, "Atomics.wait cannot be called in this context");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_big_memory_usage(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
t.query("adversarial:bigMemoryUsage", assert_obj!()).await?;
// Regression test. Previously this would error on the second query with
// "Possible memory leak: not enough room for user heap"
t.query("adversarial:bigMemoryUsage", assert_obj!()).await?;
Ok(())
}
#[convex_macro::test_runtime]
async fn test_not_implemented_builtin(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let not_implemented_error = t
.query_js_error("adversarial:useNotImplementedBuiltin", assert_obj!())
.await?;
assert_contains(¬_implemented_error, "Not implemented");
// Has the original function somewhere in the stack trace
assert_contains(¬_implemented_error, "convex/adversarial.ts:");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_stubbed_unsupported_apis(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let not_implemented_error = t
.query_js_error("adversarial:setTimeoutThrows", assert_obj!())
.await?;
assert_contains(
¬_implemented_error,
"Can't use setTimeout in queries and mutations. Please consider using an action",
);
let not_implemented_error = t
.query_js_error("adversarial:setIntervalThrows", assert_obj!())
.await?;
assert_contains(
¬_implemented_error,
"Can't use setInterval in queries and mutations. Please consider using an action",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_never_pushed(rt: TestRuntime) -> anyhow::Result<()> {
// Test that if you've never pushed functions, we generate an appropriate
// error message telling you to push.
let t = UdfTest::default(rt).await?;
let mut tx = t.database.begin(Identity::system()).await?;
// Delete the UDF config to simulate it never having existed.
let mut udf_config_model = UdfConfigModel::new(&mut tx, TableNamespace::test_user());
must_let!(let Some(config) = udf_config_model.get().await?);
SystemMetadataModel::new(&mut tx, TableNamespace::test_user())
.delete(config.id())
.await?;
let path = CanonicalizedComponentFunctionPath {
component: ComponentPath::test_user(),
udf_path: CanonicalizedUdfPath::from_str("myFunc.js:default")?,
};
let result = ValidatedPathAndArgs::new(
AllowedVisibility::PublicOnly,
&mut tx,
PublicFunctionPath::Component(path),
ConvexArray::empty(),
UdfType::Mutation,
)
.await?;
must_let!(let Err(js_error) = result);
assert_contains(&js_error, "Could not find public function for 'myFunc'");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_invoke_function_directly(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let (mut outcome, _) = t
.raw_query(
"adversarial:invokeFunctionDirectly",
vec![assert_val!({})],
Identity::system(),
None,
)
.await?;
let log_line = outcome
.log_lines
.pop()
.unwrap()
.to_pretty_string_test_only();
assert_contains(
&log_line,
"[WARN] 'Convex functions should not directly call other Convex functions. Consider \
calling a helper function instead.",
);
Ok(())
}
#[convex_macro::test_runtime]
async fn test_uncatchable_errors_are_uncatchable(rt: TestRuntime) -> anyhow::Result<()> {
UdfTest::run_test_with_isolate2(rt, async move |t| {
let (outcome, _) = t
.raw_query(
"adversarial:throwUncatchableDeveloperError",
vec![assert_val!({})],
Identity::system(),
None,
)
.await?;
assert_eq!(outcome.log_lines, LogLines::default());
assert_contains(&outcome.result.unwrap_err(), "idonotexist");
Ok(())
})
.await
}
#[convex_macro::test_runtime]
async fn test_subfunction_depth(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default_with_config(DEFAULT_CONFIG.clone(), 16, rt).await?;
let error = t
.query_js_error(
"adversarial:recursiveSubfunction",
assert_obj!("depth" => 9.0),
)
.await?;
assert_contains(&error, "Cross component call depth limit exceeded");
let result = t
.query(
"adversarial:recursiveSubfunction",
assert_obj!("depth" => 8.0),
)
.await?;
assert_eq!(result, ConvexValue::Float64(8.0));
Ok(())
}
#[convex_macro::test_runtime]
async fn test_array_buffer_size_limit(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
// Allocating an ArrayBuffer over the size limit fails
let e = t
.query_js_error(
"adversarial:allocateArrayBuffers",
assert_obj!(
"size" => (*ISOLATE_MAX_ARRAY_BUFFER_TOTAL_SIZE + 1) as f64,
"count" => 1.0,
"retain" => 1.0,
),
)
.await?;
assert_contains(&e, "RangeError: Array buffer allocation failed");
// However, we can repeatedly allocate under the size limit and succeed as
// long as the old ones can be GC'd
t.query(
"adversarial:allocateArrayBuffers",
assert_obj!(
"size" => *ISOLATE_MAX_ARRAY_BUFFER_TOTAL_SIZE as f64,
"count" => 10.0,
"retain" => 0.0,
),
)
.await?;
t.query(
"adversarial:allocateArrayBuffers",
assert_obj!(
"size" => (*ISOLATE_MAX_ARRAY_BUFFER_TOTAL_SIZE / 2) as f64,
"count" => 10.0,
"retain" => 1.0,
),
)
.await?;
// ... but retaining too many ArrayBuffers will fail
let e = t
.query_js_error(
"adversarial:allocateArrayBuffers",
assert_obj!(
"size" => (*ISOLATE_MAX_ARRAY_BUFFER_TOTAL_SIZE / 2) as f64,
"count" => 10.0,
"retain" => 2.0,
),
)
.await?;
assert_contains(&e, "RangeError: Array buffer allocation failed");
Ok(())
}
#[convex_macro::test_runtime]
async fn test_paginate_page_size_limit(rt: TestRuntime) -> anyhow::Result<()> {
let t = UdfTest::default(rt).await?;
let e = t
.query_js_error("adversarial:paginateTooManyItems", assert_obj!())
.await?;
assert_contains(&e, "Requested too many items");
Ok(())
}