Skip to content

Commit 85c1358

Browse files
committed
Add unit tests for ops.rs and ops_s3.rs
Prepare for refactoring by adding comprehensive tests: ops.rs tests: - generate_request_id format and uniqueness - try_internal_worker_route with various scenarios - wrap_query_as_json for SELECT and RETURNING queries - RunnerOperations builder pattern ops_s3.rs tests: - build_s3_url with prefix, without prefix, from full URL - Path traversal rejection (encoded and double-encoded) - Cloudflare R2 URL format
1 parent d9985aa commit 85c1358

2 files changed

Lines changed: 403 additions & 0 deletions

File tree

src/ops.rs

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,3 +1234,310 @@ async fn do_fetch(
12341234
body: ResponseBody::Stream(rx),
12351235
})
12361236
}
1237+
1238+
#[cfg(test)]
1239+
mod tests {
1240+
use super::*;
1241+
1242+
// ============================================================================
1243+
// generate_request_id tests
1244+
// ============================================================================
1245+
1246+
#[test]
1247+
fn test_generate_request_id_format() {
1248+
let id = generate_request_id("test");
1249+
// Format: prefix_hex (e.g., test_00000000000188fc94e525937a8)
1250+
// Total length is 32 chars
1251+
assert!(
1252+
id.starts_with("test_"),
1253+
"ID should start with prefix_: {}",
1254+
id
1255+
);
1256+
assert_eq!(id.len(), 32, "ID should be 32 chars: {}", id);
1257+
1258+
// Split at underscore
1259+
let parts: Vec<&str> = id.splitn(2, '_').collect();
1260+
assert_eq!(parts.len(), 2, "ID should have 2 parts: {}", id);
1261+
assert_eq!(parts[0], "test");
1262+
// Hex part should be 27 chars (31 - len("test"))
1263+
assert_eq!(
1264+
parts[1].len(),
1265+
27,
1266+
"Hex part should be 27 chars: {}",
1267+
parts[1]
1268+
);
1269+
// Should be valid hex
1270+
assert!(
1271+
u128::from_str_radix(parts[1], 16).is_ok(),
1272+
"Hex part should be valid hex: {}",
1273+
parts[1]
1274+
);
1275+
}
1276+
1277+
#[test]
1278+
fn test_generate_request_id_unique() {
1279+
let id1 = generate_request_id("test");
1280+
let id2 = generate_request_id("test");
1281+
// IDs are based on nanoseconds, so they should be different
1282+
// (unless generated in same nanosecond, which is very unlikely)
1283+
assert_ne!(id1, id2, "IDs should be unique");
1284+
}
1285+
1286+
// ============================================================================
1287+
// try_internal_worker_route tests
1288+
//
1289+
// Note: These tests require WORKER_DOMAINS env var to be set.
1290+
// In production, this is typically "workers.rocks,workers.dev.localhost"
1291+
// Tests that require routing will skip if WORKER_DOMAINS is empty.
1292+
// ============================================================================
1293+
1294+
fn has_worker_domains() -> bool {
1295+
!WORKER_DOMAINS.is_empty()
1296+
}
1297+
1298+
#[test]
1299+
fn test_internal_route_workers_rocks() {
1300+
if !has_worker_domains() {
1301+
eprintln!("Skipping: WORKER_DOMAINS not set");
1302+
return;
1303+
}
1304+
1305+
// Use first configured domain
1306+
let domain = &WORKER_DOMAINS[0];
1307+
let url = format!("https://my-worker.{}/api/test?foo=bar", domain);
1308+
1309+
let request = HttpRequest {
1310+
method: HttpMethod::Get,
1311+
url,
1312+
headers: HashMap::new(),
1313+
body: RequestBody::None,
1314+
};
1315+
1316+
let routed = try_internal_worker_route(&request);
1317+
assert!(routed.is_some(), "Should route configured domain URLs");
1318+
1319+
let routed = routed.unwrap();
1320+
assert_eq!(routed.url, "http://127.0.0.1:8080/api/test?foo=bar");
1321+
assert_eq!(
1322+
routed.headers.get("x-worker-name"),
1323+
Some(&"my-worker".to_string())
1324+
);
1325+
assert!(routed.headers.contains_key("x-request-id"));
1326+
}
1327+
1328+
#[test]
1329+
fn test_internal_route_preserves_method() {
1330+
if !has_worker_domains() {
1331+
eprintln!("Skipping: WORKER_DOMAINS not set");
1332+
return;
1333+
}
1334+
1335+
let domain = &WORKER_DOMAINS[0];
1336+
let request = HttpRequest {
1337+
method: HttpMethod::Post,
1338+
url: format!("https://api.{}/data", domain),
1339+
headers: HashMap::new(),
1340+
body: RequestBody::None,
1341+
};
1342+
1343+
let routed = try_internal_worker_route(&request).unwrap();
1344+
assert_eq!(routed.method, HttpMethod::Post);
1345+
}
1346+
1347+
#[test]
1348+
fn test_internal_route_preserves_body() {
1349+
if !has_worker_domains() {
1350+
eprintln!("Skipping: WORKER_DOMAINS not set");
1351+
return;
1352+
}
1353+
1354+
let domain = &WORKER_DOMAINS[0];
1355+
let body_data = b"test body data".to_vec();
1356+
let request = HttpRequest {
1357+
method: HttpMethod::Post,
1358+
url: format!("https://api.{}/data", domain),
1359+
headers: HashMap::new(),
1360+
body: RequestBody::Bytes(body_data.clone().into()),
1361+
};
1362+
1363+
let routed = try_internal_worker_route(&request).unwrap();
1364+
match routed.body {
1365+
RequestBody::Bytes(b) => assert_eq!(b.as_ref(), body_data.as_slice()),
1366+
_ => panic!("Expected Bytes body"),
1367+
}
1368+
}
1369+
1370+
#[test]
1371+
fn test_internal_route_no_match_external() {
1372+
// This test always works - external URLs should never match
1373+
let request = HttpRequest {
1374+
method: HttpMethod::Get,
1375+
url: "https://example.com/api".to_string(),
1376+
headers: HashMap::new(),
1377+
body: RequestBody::None,
1378+
};
1379+
1380+
let routed = try_internal_worker_route(&request);
1381+
assert!(routed.is_none(), "Should not route external URLs");
1382+
}
1383+
1384+
#[test]
1385+
fn test_internal_route_no_match_partial() {
1386+
if WORKER_DOMAINS.is_empty() {
1387+
eprintln!("Skipping: WORKER_DOMAINS not set");
1388+
return;
1389+
}
1390+
1391+
// Should not match bare domain without subdomain
1392+
let domain = &WORKER_DOMAINS[0];
1393+
let request = HttpRequest {
1394+
method: HttpMethod::Get,
1395+
url: format!("https://{}/api", domain),
1396+
headers: HashMap::new(),
1397+
body: RequestBody::None,
1398+
};
1399+
1400+
let routed = try_internal_worker_route(&request);
1401+
assert!(routed.is_none(), "Should not route bare domain");
1402+
}
1403+
1404+
#[test]
1405+
fn test_internal_route_stream_body_not_supported() {
1406+
if !has_worker_domains() {
1407+
eprintln!("Skipping: WORKER_DOMAINS not set");
1408+
return;
1409+
}
1410+
1411+
let domain = &WORKER_DOMAINS[0];
1412+
let (_tx, rx) = tokio::sync::mpsc::channel(1);
1413+
let request = HttpRequest {
1414+
method: HttpMethod::Post,
1415+
url: format!("https://api.{}/data", domain),
1416+
headers: HashMap::new(),
1417+
body: RequestBody::Stream(rx),
1418+
};
1419+
1420+
let routed = try_internal_worker_route(&request);
1421+
assert!(routed.is_none(), "Should not route streaming bodies");
1422+
}
1423+
1424+
#[test]
1425+
fn test_internal_route_path_only() {
1426+
if !has_worker_domains() {
1427+
eprintln!("Skipping: WORKER_DOMAINS not set");
1428+
return;
1429+
}
1430+
1431+
let domain = &WORKER_DOMAINS[0];
1432+
let request = HttpRequest {
1433+
method: HttpMethod::Get,
1434+
url: format!("https://worker.{}/", domain),
1435+
headers: HashMap::new(),
1436+
body: RequestBody::None,
1437+
};
1438+
1439+
let routed = try_internal_worker_route(&request).unwrap();
1440+
assert_eq!(routed.url, "http://127.0.0.1:8080/");
1441+
}
1442+
1443+
#[test]
1444+
fn test_internal_route_complex_path() {
1445+
if !has_worker_domains() {
1446+
eprintln!("Skipping: WORKER_DOMAINS not set");
1447+
return;
1448+
}
1449+
1450+
let domain = &WORKER_DOMAINS[0];
1451+
let request = HttpRequest {
1452+
method: HttpMethod::Get,
1453+
url: format!(
1454+
"https://app.{}/api/v1/users/123?include=profile&format=json",
1455+
domain
1456+
),
1457+
headers: HashMap::new(),
1458+
body: RequestBody::None,
1459+
};
1460+
1461+
let routed = try_internal_worker_route(&request).unwrap();
1462+
assert_eq!(
1463+
routed.url,
1464+
"http://127.0.0.1:8080/api/v1/users/123?include=profile&format=json"
1465+
);
1466+
assert_eq!(
1467+
routed.headers.get("x-worker-name"),
1468+
Some(&"app".to_string())
1469+
);
1470+
}
1471+
1472+
// ============================================================================
1473+
// wrap_query_as_json tests (database feature only)
1474+
// ============================================================================
1475+
1476+
#[cfg(feature = "database")]
1477+
mod database_tests {
1478+
use super::super::*;
1479+
1480+
#[test]
1481+
fn test_wrap_select_query() {
1482+
let sql = "SELECT * FROM users WHERE id = $1";
1483+
let wrapped = wrap_query_as_json(sql, QueryMode::Select);
1484+
assert!(wrapped.contains("jsonb_agg"));
1485+
assert!(wrapped.contains("row_to_json"));
1486+
assert!(wrapped.contains(sql));
1487+
}
1488+
1489+
#[test]
1490+
fn test_wrap_select_trims_semicolon() {
1491+
let sql = "SELECT * FROM users;";
1492+
let wrapped = wrap_query_as_json(sql, QueryMode::Select);
1493+
assert!(!wrapped.contains(";;"), "Should not have double semicolons");
1494+
}
1495+
1496+
#[test]
1497+
fn test_wrap_returning_mutation() {
1498+
let sql = "INSERT INTO users (name) VALUES ($1) RETURNING *";
1499+
let wrapped = wrap_query_as_json(sql, QueryMode::ReturningMutation);
1500+
assert!(wrapped.starts_with("WITH t AS"));
1501+
assert!(wrapped.contains(sql.trim_end_matches(';')));
1502+
}
1503+
1504+
#[test]
1505+
#[should_panic(expected = "Mutation queries should not be wrapped")]
1506+
fn test_wrap_mutation_panics() {
1507+
let sql = "DELETE FROM users WHERE id = $1";
1508+
wrap_query_as_json(sql, QueryMode::Mutation);
1509+
}
1510+
}
1511+
1512+
// ============================================================================
1513+
// RunnerOperations construction tests
1514+
// ============================================================================
1515+
1516+
#[test]
1517+
fn test_runner_operations_builder() {
1518+
let ops = RunnerOperations::new();
1519+
assert!(ops.bindings.assets.is_empty());
1520+
assert!(ops.bindings.storage.is_empty());
1521+
assert!(ops.bindings.kv.is_empty());
1522+
}
1523+
1524+
#[test]
1525+
fn test_runner_operations_with_user() {
1526+
let ops = RunnerOperations::new().with_user_id("user-123".to_string());
1527+
assert_eq!(ops.user_id, Some("user-123".to_string()));
1528+
}
1529+
1530+
#[test]
1531+
fn test_runner_operations_with_worker() {
1532+
let ops = RunnerOperations::new().with_worker_id("worker-456".to_string());
1533+
assert_eq!(ops.worker_id, Some("worker-456".to_string()));
1534+
}
1535+
1536+
#[test]
1537+
fn test_runner_operations_stats_initial() {
1538+
let ops = RunnerOperations::new();
1539+
assert_eq!(ops.stats.fetch_count.load(Ordering::Relaxed), 0);
1540+
assert_eq!(ops.stats.fetch_bytes_in.load(Ordering::Relaxed), 0);
1541+
assert_eq!(ops.stats.fetch_bytes_out.load(Ordering::Relaxed), 0);
1542+
}
1543+
}

0 commit comments

Comments
 (0)