use super::*;
use chrono::{DateTime, TimeZone, Utc};
use proptest::prelude::*;
use test_r::test;
fn datetime_strategy() -> impl Strategy<Value = DateTime<Utc>> {
(0i64..253_402_214_400i64, 0u32..1_000_000_000).prop_map(|(s, n)| {
Utc.timestamp_opt(s, n)
.single()
.expect("strategy bounds keep timestamps valid")
})
}
fn payload_strategy() -> impl Strategy<Value = QuotaTokenValuePayload> {
(
any::<u64>(),
any::<u64>(),
"[a-z][a-z0-9_-]{0,15}",
any::<u64>(),
any::<i64>(),
datetime_strategy(),
)
.prop_map(
|(hi, lo, resource_name, expected_use, last_credit, last_credit_at)| {
QuotaTokenValuePayload {
environment_id: EnvironmentId::new(uuid::Uuid::from_u64_pair(hi, lo)),
resource_name,
expected_use,
last_credit,
last_credit_at,
}
},
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn text_round_trip(p in payload_strategy()) {
let s = to_text(&p).expect("to_text");
let back = from_text(&s).expect("from_text");
prop_assert_eq!(back, p);
}
#[test]
fn json_round_trip(p in payload_strategy()) {
let j = to_json(&p).expect("to_json");
let back = from_json(&j).expect("from_json");
prop_assert_eq!(back, p);
}
}
#[test]
fn empty_text_rejected() {
assert_eq!(from_text(""), Err(ParseError::Empty));
}
#[test]
fn missing_prefix_rejected() {
assert!(matches!(
from_text("garbage"),
Err(ParseError::BadFormat(_))
));
}
#[test]
fn prefix_only_rejected() {
assert_eq!(from_text("quota-token:"), Err(ParseError::Empty));
}
#[test]
fn bad_base64_rejected() {
assert!(matches!(
from_text("quota-token:!!!"),
Err(ParseError::InvalidBase64(_))
));
}
#[test]
fn json_wrong_type() {
assert_eq!(
from_json(&Value::String("x".into())),
Err(ParseError::TypeField {
expected: "object",
field: None,
})
);
}
#[test]
fn json_missing_field() {
let v = serde_json::json!({
"environmentId": "00000000-0000-0000-0000-000000000000",
"resourceName": "r",
"expectedUse": "1",
"lastCredit": "0"
});
assert_eq!(from_json(&v), Err(ParseError::MissingField("lastCreditAt")));
}
#[test]
fn json_extra_field_rejected() {
let v = serde_json::json!({
"environmentId": "00000000-0000-0000-0000-000000000000",
"resourceName": "r",
"expectedUse": "1",
"lastCredit": "0",
"lastCreditAt": "2025-01-01T00:00:00Z",
"stowaway": true
});
assert_eq!(
from_json(&v),
Err(ParseError::ExtraField("stowaway".into()))
);
}
#[test]
fn expected_use_accepts_number_or_string() {
let base = serde_json::json!({
"environmentId": "00000000-0000-0000-0000-000000000000",
"resourceName": "r",
"expectedUse": 42,
"lastCredit": -1,
"lastCreditAt": "2025-01-01T00:00:00Z"
});
let v_num = from_json(&base).expect("number form");
assert_eq!(v_num.expected_use, 42);
assert_eq!(v_num.last_credit, -1);
let mut s = base.as_object().expect("base obj").clone();
s.insert(
FIELD_EXPECTED_USE.into(),
Value::String("18446744073709551615".into()), );
s.insert(
FIELD_LAST_CREDIT.into(),
Value::String("-9223372036854775808".into()), );
let v_str = from_json(&Value::Object(s)).expect("string form");
assert_eq!(v_str.expected_use, u64::MAX);
assert_eq!(v_str.last_credit, i64::MIN);
}
#[test]
fn output_uses_string_for_numeric_fields() {
let p = QuotaTokenValuePayload {
environment_id: EnvironmentId::new(uuid::Uuid::nil()),
resource_name: "r".into(),
expected_use: u64::MAX,
last_credit: i64::MIN,
last_credit_at: chrono::Utc.timestamp_opt(0, 0).single().unwrap(),
};
let v = to_json(&p).expect("to_json");
let obj = v.as_object().expect("object");
assert_eq!(
obj.get(FIELD_EXPECTED_USE),
Some(&Value::String(u64::MAX.to_string()))
);
assert_eq!(
obj.get(FIELD_LAST_CREDIT),
Some(&Value::String(i64::MIN.to_string()))
);
}
#[test]
fn invalid_uuid_rejected() {
let v = serde_json::json!({
"environmentId": "not a uuid",
"resourceName": "r",
"expectedUse": "1",
"lastCredit": "0",
"lastCreditAt": "2025-01-01T00:00:00Z"
});
assert!(matches!(from_json(&v), Err(ParseError::BadFormat(_))));
}
#[test]
fn wrong_expected_use_type_rejected() {
let v = serde_json::json!({
"environmentId": "00000000-0000-0000-0000-000000000000",
"resourceName": "r",
"expectedUse": true,
"lastCredit": "0",
"lastCreditAt": "2025-01-01T00:00:00Z"
});
assert_eq!(
from_json(&v),
Err(ParseError::TypeField {
expected: "number or string",
field: Some("expectedUse"),
})
);
}
#[test]
fn wrong_last_credit_type_rejected() {
let v = serde_json::json!({
"environmentId": "00000000-0000-0000-0000-000000000000",
"resourceName": "r",
"expectedUse": "0",
"lastCredit": true,
"lastCreditAt": "2025-01-01T00:00:00Z"
});
assert_eq!(
from_json(&v),
Err(ParseError::TypeField {
expected: "number or string",
field: Some("lastCredit"),
})
);
}
#[test]
fn invalid_last_credit_at_rejected() {
let v = serde_json::json!({
"environmentId": "00000000-0000-0000-0000-000000000000",
"resourceName": "r",
"expectedUse": "0",
"lastCredit": "0",
"lastCreditAt": "not a date"
});
assert!(matches!(from_json(&v), Err(ParseError::Nested(_))));
}