use dynoxide::Database;
use dynoxide::DynoxideError;
use dynoxide::actions::batch_get_item::BatchGetItemRequest;
use dynoxide::actions::batch_write_item::BatchWriteItemRequest;
use dynoxide::actions::create_table::CreateTableRequest;
use dynoxide::actions::put_item::PutItemRequest;
fn setup_db() -> Database {
Database::memory().unwrap()
}
fn create_test_table(db: &Database, name: &str) {
let req: CreateTableRequest = serde_json::from_value(serde_json::json!({
"TableName": name,
"KeySchema": [
{"AttributeName": "pk", "KeyType": "HASH"},
{"AttributeName": "sk", "KeyType": "RANGE"}
],
"AttributeDefinitions": [
{"AttributeName": "pk", "AttributeType": "S"},
{"AttributeName": "sk", "AttributeType": "S"}
],
"BillingMode": "PAY_PER_REQUEST"
}))
.unwrap();
db.create_table(req).unwrap();
}
fn put(db: &Database, table: &str, item: serde_json::Value) {
let req: PutItemRequest = serde_json::from_value(serde_json::json!({
"TableName": table,
"Item": item
}))
.unwrap();
db.put_item(req).unwrap();
}
#[test]
fn test_batch_get_single_table() {
let db = setup_db();
create_test_table(&db, "Tbl");
put(
&db,
"Tbl",
serde_json::json!({"pk": {"S": "a"}, "sk": {"S": "1"}, "name": {"S": "Alice"}}),
);
put(
&db,
"Tbl",
serde_json::json!({"pk": {"S": "b"}, "sk": {"S": "1"}, "name": {"S": "Bob"}}),
);
let req: BatchGetItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": {
"Keys": [
{"pk": {"S": "a"}, "sk": {"S": "1"}},
{"pk": {"S": "b"}, "sk": {"S": "1"}}
]
}
}
}))
.unwrap();
let resp = db.batch_get_item(req).unwrap();
assert_eq!(resp.responses["Tbl"].len(), 2);
assert!(resp.unprocessed_keys.is_empty());
}
#[test]
fn test_batch_get_multiple_tables() {
let db = setup_db();
create_test_table(&db, "TableA");
create_test_table(&db, "TableB");
put(
&db,
"TableA",
serde_json::json!({"pk": {"S": "a"}, "sk": {"S": "1"}}),
);
put(
&db,
"TableB",
serde_json::json!({"pk": {"S": "b"}, "sk": {"S": "1"}}),
);
let req: BatchGetItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"TableA": {"Keys": [{"pk": {"S": "a"}, "sk": {"S": "1"}}]},
"TableB": {"Keys": [{"pk": {"S": "b"}, "sk": {"S": "1"}}]}
}
}))
.unwrap();
let resp = db.batch_get_item(req).unwrap();
assert_eq!(resp.responses["TableA"].len(), 1);
assert_eq!(resp.responses["TableB"].len(), 1);
}
#[test]
fn test_batch_get_with_projection() {
let db = setup_db();
create_test_table(&db, "Tbl");
put(
&db,
"Tbl",
serde_json::json!({"pk": {"S": "a"}, "sk": {"S": "1"}, "name": {"S": "Alice"}, "age": {"N": "30"}}),
);
let req: BatchGetItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": {
"Keys": [{"pk": {"S": "a"}, "sk": {"S": "1"}}],
"ProjectionExpression": "#n",
"ExpressionAttributeNames": {"#n": "name"}
}
}
}))
.unwrap();
let resp = db.batch_get_item(req).unwrap();
let items = &resp.responses["Tbl"];
assert_eq!(items.len(), 1);
assert!(!items[0].contains_key("pk"));
assert!(items[0].contains_key("name")); assert!(!items[0].contains_key("age")); }
#[test]
fn test_batch_get_key_not_found() {
let db = setup_db();
create_test_table(&db, "Tbl");
put(
&db,
"Tbl",
serde_json::json!({"pk": {"S": "a"}, "sk": {"S": "1"}}),
);
let req: BatchGetItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": {
"Keys": [
{"pk": {"S": "a"}, "sk": {"S": "1"}},
{"pk": {"S": "missing"}, "sk": {"S": "1"}}
]
}
}
}))
.unwrap();
let resp = db.batch_get_item(req).unwrap();
assert_eq!(resp.responses["Tbl"].len(), 1); }
#[test]
fn test_batch_get_exceeds_100_keys() {
let db = setup_db();
create_test_table(&db, "Tbl");
let keys: Vec<serde_json::Value> = (0..101)
.map(|i| serde_json::json!({"pk": {"S": format!("k{}", i)}, "sk": {"S": "x"}}))
.collect();
let req: BatchGetItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": {"Keys": keys}
}
}))
.unwrap();
let err = db.batch_get_item(req).unwrap_err();
let msg = format!("{err:?}");
assert!(
msg.contains("Member must have length less than or equal to 100"),
"Got: {msg}"
);
assert!(msg.contains("RequestItems.Tbl.member.Keys"), "Got: {msg}");
}
#[test]
fn test_batch_write_puts_and_deletes() {
let db = setup_db();
create_test_table(&db, "Tbl");
put(
&db,
"Tbl",
serde_json::json!({"pk": {"S": "del"}, "sk": {"S": "1"}}),
);
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": [
{"PutRequest": {"Item": {"pk": {"S": "new1"}, "sk": {"S": "1"}, "val": {"S": "hello"}}}},
{"PutRequest": {"Item": {"pk": {"S": "new2"}, "sk": {"S": "1"}, "val": {"S": "world"}}}},
{"DeleteRequest": {"Key": {"pk": {"S": "del"}, "sk": {"S": "1"}}}}
]
}
}))
.unwrap();
let resp = db.batch_write_item(req).unwrap();
assert!(resp.unprocessed_items.is_empty());
let scan = db
.scan(serde_json::from_value(serde_json::json!({"TableName": "Tbl"})).unwrap())
.unwrap();
assert_eq!(scan.count, 2); }
#[test]
fn test_batch_write_multiple_tables() {
let db = setup_db();
create_test_table(&db, "TableA");
create_test_table(&db, "TableB");
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"TableA": [
{"PutRequest": {"Item": {"pk": {"S": "a"}, "sk": {"S": "1"}}}}
],
"TableB": [
{"PutRequest": {"Item": {"pk": {"S": "b"}, "sk": {"S": "1"}}}}
]
}
}))
.unwrap();
db.batch_write_item(req).unwrap();
let scan_a = db
.scan(serde_json::from_value(serde_json::json!({"TableName": "TableA"})).unwrap())
.unwrap();
let scan_b = db
.scan(serde_json::from_value(serde_json::json!({"TableName": "TableB"})).unwrap())
.unwrap();
assert_eq!(scan_a.count, 1);
assert_eq!(scan_b.count, 1);
}
#[test]
fn test_batch_write_exceeds_25_items() {
let db = setup_db();
create_test_table(&db, "Tbl");
let items: Vec<serde_json::Value> = (0..26)
.map(|i| {
serde_json::json!({"PutRequest": {"Item": {"pk": {"S": format!("k{}", i)}, "sk": {"S": "x"}}}})
})
.collect();
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": items
}
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
let msg = format!("{err:?}");
assert!(
msg.contains("Member must have length less than or equal to 25"),
"Got: {msg}"
);
assert!(msg.contains("at 'requestItems'"), "Got: {msg}");
}
#[test]
fn test_batch_write_item_too_large() {
let db = setup_db();
create_test_table(&db, "Tbl");
let big_string = "x".repeat(400 * 1024 + 1);
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": [
{"PutRequest": {"Item": {"pk": {"S": "a"}, "sk": {"S": "1"}, "data": {"S": big_string}}}}
]
}
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
assert!(format!("{err:?}").contains("Item size"), "Got: {err:?}");
}
#[test]
fn test_batch_write_nonexistent_table() {
let db = setup_db();
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"NonExistent": [
{"PutRequest": {"Item": {"pk": {"S": "a"}, "sk": {"S": "1"}}}}
]
}
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
assert!(format!("{err:?}").contains("not found"), "Got: {err:?}");
}
#[test]
fn test_batch_write_keyless_put_rejected_with_400() {
let db = setup_db();
let req: CreateTableRequest = serde_json::from_value(serde_json::json!({
"TableName": "HashTbl",
"KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
"AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
"BillingMode": "PAY_PER_REQUEST"
}))
.unwrap();
db.create_table(req).unwrap();
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"HashTbl": [
{"PutRequest": {"Item": {"notkey": {"S": "x"}}}}
]
}
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
assert!(
matches!(err, DynoxideError::ValidationException(_)),
"key-less Put must be a ValidationException, got: {err:?}"
);
assert_eq!(err.status_code(), 400, "must be HTTP 400, got: {err:?}");
}
const BATCH_KEY_SCHEMA_MSG: &str = "The provided key element does not match the schema";
#[test]
fn test_batch_write_wrong_type_table_key_returns_schema_error() {
let db = setup_db();
create_test_table(&db, "Tbl");
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": [
{"PutRequest": {"Item": {"pk": {"N": "1"}, "sk": {"S": "a"}}}}
]
}
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
assert!(
matches!(&err, DynoxideError::ValidationException(m) if m == BATCH_KEY_SCHEMA_MSG),
"wrong-type batch table key must return the generic schema error, got: {err:?}"
);
}
#[test]
fn test_batch_write_non_scalar_table_key_returns_schema_error() {
let db = setup_db();
create_test_table(&db, "Tbl");
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": [
{"PutRequest": {"Item": {"pk": {"BOOL": true}, "sk": {"S": "a"}}}}
]
}
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
assert!(
matches!(&err, DynoxideError::ValidationException(m) if m == BATCH_KEY_SCHEMA_MSG),
"non-scalar batch table key must return the generic schema error, got: {err:?}"
);
}
#[test]
fn test_batch_write_wrong_type_sort_key_returns_schema_error() {
let db = setup_db();
create_test_table(&db, "Tbl");
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": [
{"PutRequest": {"Item": {"pk": {"S": "a"}, "sk": {"N": "1"}}}}
]
}
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
assert!(
matches!(&err, DynoxideError::ValidationException(m) if m == BATCH_KEY_SCHEMA_MSG),
"wrong-type batch sort key must return the generic schema error, got: {err:?}"
);
}
#[test]
fn test_put_item_wrong_type_key_keeps_type_mismatch_message() {
let db = setup_db();
create_test_table(&db, "Tbl");
let req: PutItemRequest = serde_json::from_value(serde_json::json!({
"TableName": "Tbl",
"Item": {"pk": {"N": "1"}, "sk": {"S": "a"}}
}))
.unwrap();
let err = db.put_item(req).unwrap_err();
assert!(
matches!(&err, DynoxideError::ValidationException(m) if m.contains("Type mismatch for key")),
"PutItem must keep the specific type-mismatch message, got: {err:?}"
);
}
#[test]
fn test_batch_get_nonexistent_table() {
let db = setup_db();
let req: BatchGetItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"NonExistent": {
"Keys": [{"pk": {"S": "a"}, "sk": {"S": "1"}}]
}
}
}))
.unwrap();
let err = db.batch_get_item(req).unwrap_err();
assert!(format!("{err:?}").contains("not found"), "Got: {err:?}");
}
#[test]
fn test_batch_get_returns_unprocessed_keys_over_16mb() {
let db = setup_db();
let req: CreateTableRequest = serde_json::from_value(serde_json::json!({
"TableName": "BigTbl",
"KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
"AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
"ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}
}))
.unwrap();
db.create_table(req).unwrap();
let big_val = "x".repeat(350 * 1024);
for i in 0..50 {
let item_req: PutItemRequest = serde_json::from_value(serde_json::json!({
"TableName": "BigTbl",
"Item": {"pk": {"S": format!("k{i}")}, "data": {"S": big_val}}
}))
.unwrap();
db.put_item(item_req).unwrap();
}
let keys: Vec<serde_json::Value> = (0..50)
.map(|i| serde_json::json!({"pk": {"S": format!("k{i}")}}))
.collect();
let req: BatchGetItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"BigTbl": {"Keys": keys}
}
}))
.unwrap();
let resp = db.batch_get_item(req).unwrap();
let returned = resp.responses.get("BigTbl").map_or(0, |v| v.len());
assert!(returned > 0, "Should have returned some items");
assert!(
returned < 50,
"Should not have returned all 50 items (16MB limit), got {returned}"
);
assert!(
!resp.unprocessed_keys.is_empty(),
"Should have unprocessed keys"
);
}
#[test]
fn test_batch_write_exceeds_16mb_aggregate() {
let db = setup_db();
let req: CreateTableRequest = serde_json::from_value(serde_json::json!({
"TableName": "BigTbl",
"KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
"AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
"ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}
}))
.unwrap();
db.create_table(req).unwrap();
let big_val = "x".repeat(300 * 1024);
let items: Vec<serde_json::Value> = (0..25)
.map(|i| {
serde_json::json!({"PutRequest": {"Item": {"pk": {"S": format!("k{i}")}, "data": {"S": big_val}}}})
})
.collect();
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"BigTbl": items
}
}))
.unwrap();
db.batch_write_item(req).unwrap();
}
const BATCH_EMPTY_KEY_MSG: &str = "One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: pk";
#[test]
fn test_batch_write_delete_empty_string_key_is_top_level_validation() {
let db = setup_db();
create_test_table(&db, "Tbl");
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": [
{"DeleteRequest": {"Key": {"pk": {"S": ""}, "sk": {"S": "a"}}}}
]
}
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
assert_eq!(
err.error_type(),
"com.amazon.coral.validate#ValidationException"
);
assert_eq!(err.status_code(), 400, "must be HTTP 400, got: {err:?}");
assert_eq!(err.to_string(), BATCH_EMPTY_KEY_MSG);
}
#[test]
fn test_batch_get_empty_string_key_is_top_level_validation() {
let db = setup_db();
create_test_table(&db, "Tbl");
let req: BatchGetItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": {
"Tbl": {"Keys": [{"pk": {"S": ""}, "sk": {"S": "a"}}]}
}
}))
.unwrap();
let err = db.batch_get_item(req).unwrap_err();
assert_eq!(
err.error_type(),
"com.amazon.coral.validate#ValidationException"
);
assert_eq!(err.status_code(), 400, "must be HTTP 400, got: {err:?}");
assert_eq!(err.to_string(), BATCH_EMPTY_KEY_MSG);
}
const BATCH_EMPTY_BINARY_MSG: &str = "One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty binary value. Key: pk";
fn create_binary_key_table(db: &Database, name: &str) {
let req: CreateTableRequest = serde_json::from_value(serde_json::json!({
"TableName": name,
"KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
"AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "B"}]
}))
.unwrap();
db.create_table(req).unwrap();
}
#[test]
fn test_batch_write_delete_empty_binary_key_is_top_level_validation() {
let db = setup_db();
create_binary_key_table(&db, "BinTbl");
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": { "BinTbl": [{ "DeleteRequest": { "Key": { "pk": { "B": "" } } } }] }
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
assert_eq!(
err.error_type(),
"com.amazon.coral.validate#ValidationException"
);
assert_eq!(err.status_code(), 400, "must be HTTP 400, got: {err:?}");
assert_eq!(err.to_string(), BATCH_EMPTY_BINARY_MSG);
}
#[test]
fn test_batch_write_put_empty_binary_key_is_top_level_validation() {
let db = setup_db();
create_binary_key_table(&db, "BinTbl");
let req: BatchWriteItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": { "BinTbl": [{ "PutRequest": { "Item": { "pk": { "B": "" } } } }] }
}))
.unwrap();
let err = db.batch_write_item(req).unwrap_err();
assert_eq!(
err.error_type(),
"com.amazon.coral.validate#ValidationException"
);
assert_eq!(err.status_code(), 400, "must be HTTP 400, got: {err:?}");
assert_eq!(err.to_string(), BATCH_EMPTY_BINARY_MSG);
}
#[test]
fn test_batch_get_empty_binary_key_is_top_level_validation() {
let db = setup_db();
create_binary_key_table(&db, "BinTbl");
let req: BatchGetItemRequest = serde_json::from_value(serde_json::json!({
"RequestItems": { "BinTbl": { "Keys": [{ "pk": { "B": "" } }] } }
}))
.unwrap();
let err = db.batch_get_item(req).unwrap_err();
assert_eq!(
err.error_type(),
"com.amazon.coral.validate#ValidationException"
);
assert_eq!(err.status_code(), 400, "must be HTTP 400, got: {err:?}");
assert_eq!(err.to_string(), BATCH_EMPTY_BINARY_MSG);
}