use kevy_resp::Argv;
use kevy_store::Store;
fn argv(parts: &[&[u8]]) -> Argv {
let mut a = Argv::default();
for p in parts {
a.push(p);
}
a
}
const REDLOCK_UNLOCK: &[u8] = b"\
if redis.call('GET', KEYS[1]) == ARGV[1] then\n\
return redis.call('DEL', KEYS[1])\n\
else\n\
return 0\n\
end\n";
const REDLOCK_EXTEND: &[u8] = b"\
if redis.call('GET', KEYS[1]) == ARGV[1] then\n\
return redis.call('PEXPIRE', KEYS[1], ARGV[2])\n\
else\n\
return 0\n\
end\n";
const ATOMIC_INCR_OR_INIT: &[u8] = b"\
if redis.call('EXISTS', KEYS[1]) == 0 then\n\
redis.call('SET', KEYS[1], ARGV[1])\n\
if tonumber(ARGV[3]) > 0 then\n\
redis.call('EXPIRE', KEYS[1], ARGV[3])\n\
end\n\
end\n\
return redis.call('INCRBY', KEYS[1], ARGV[2])\n";
#[test]
fn redlock_unlock_success_path() {
let mut store = Store::new();
let _ = kevy::dispatch(
&mut store,
&argv(&[b"SET", b"lock:order:42", b"client-A-token"]),
);
let reply = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
REDLOCK_UNLOCK,
b"1",
b"lock:order:42",
b"client-A-token",
]),
);
assert_eq!(reply, b":1\r\n");
assert_eq!(
kevy::dispatch(&mut store, &argv(&[b"GET", b"lock:order:42"])),
b"$-1\r\n",
);
}
#[test]
fn redlock_unlock_wrong_token_returns_zero_and_preserves_lock() {
let mut store = Store::new();
let _ = kevy::dispatch(
&mut store,
&argv(&[b"SET", b"lock:foo", b"someone-elses-token"]),
);
let reply = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
REDLOCK_UNLOCK,
b"1",
b"lock:foo",
b"my-token",
]),
);
assert_eq!(reply, b":0\r\n");
assert_eq!(
kevy::dispatch(&mut store, &argv(&[b"GET", b"lock:foo"])),
b"$19\r\nsomeone-elses-token\r\n",
);
}
#[test]
fn redlock_unlock_missing_key_returns_zero() {
let mut store = Store::new();
let reply = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
REDLOCK_UNLOCK,
b"1",
b"lock:never-acquired",
b"any-token",
]),
);
assert_eq!(reply, b":0\r\n");
}
#[test]
fn redlock_extend_success_path() {
let mut store = Store::new();
let _ = kevy::dispatch(&mut store, &argv(&[b"SET", b"lock:x", b"token-1"]));
let reply = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
REDLOCK_EXTEND,
b"1",
b"lock:x",
b"token-1",
b"30000",
]),
);
assert_eq!(reply, b":1\r\n");
}
#[test]
fn redlock_extend_wrong_token_returns_zero() {
let mut store = Store::new();
let _ = kevy::dispatch(&mut store, &argv(&[b"SET", b"lock:y", b"other"]));
let reply = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
REDLOCK_EXTEND,
b"1",
b"lock:y",
b"mine",
b"30000",
]),
);
assert_eq!(reply, b":0\r\n");
}
#[test]
fn atomic_incr_or_init_fresh_key() {
let mut store = Store::new();
let reply = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
ATOMIC_INCR_OR_INIT,
b"1",
b"counter:visits",
b"100",
b"1",
b"0",
]),
);
assert_eq!(reply, b":101\r\n");
assert_eq!(
kevy::dispatch(
&mut store,
&argv(&[b"GET", b"counter:visits"])
),
b"$3\r\n101\r\n",
);
}
#[test]
fn atomic_incr_or_init_existing_key_skips_init() {
let mut store = Store::new();
let _ = kevy::dispatch(
&mut store,
&argv(&[b"SET", b"counter:hits", b"5"]),
);
let reply = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
ATOMIC_INCR_OR_INIT,
b"1",
b"counter:hits",
b"100", b"10",
b"0",
]),
);
assert_eq!(reply, b":15\r\n");
}
#[test]
fn atomic_incr_or_init_with_ttl_sets_expire() {
let mut store = Store::new();
let _ = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
ATOMIC_INCR_OR_INIT,
b"1",
b"counter:ttl",
b"0",
b"7",
b"60",
]),
);
let ttl_reply = kevy::dispatch(&mut store, &argv(&[b"TTL", b"counter:ttl"]));
assert!(ttl_reply.starts_with(b":"));
let ttl_str = std::str::from_utf8(&ttl_reply[1..ttl_reply.len() - 2]).unwrap();
let ttl: i64 = ttl_str.parse().unwrap();
assert!(ttl > 0 && ttl <= 60, "got ttl: {ttl}");
}
#[test]
#[ignore]
fn sliding_window_rate_limiter_first_request_allowed() {
const SCRIPT: &[u8] = b"\
local key = KEYS[1]\n\
local now = tonumber(ARGV[1])\n\
local window = tonumber(ARGV[2])\n\
local limit = tonumber(ARGV[3])\n\
local id = ARGV[4]\n\
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)\n\
local count = redis.call('ZCARD', key)\n\
if count < limit then\n\
redis.call('ZADD', key, now, id)\n\
redis.call('PEXPIRE', key, window)\n\
return 1\n\
end\n\
return 0\n";
let mut store = Store::new();
let reply = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
SCRIPT,
b"1",
b"ratelimit:user:42",
b"1700000000000", b"60000", b"5", b"req-1",
]),
);
assert_eq!(reply, b":1\r\n");
}
#[test]
#[ignore]
fn sliding_window_rate_limiter_over_limit_rejected() {
const SCRIPT: &[u8] = b"\
local key = KEYS[1]\n\
local now = tonumber(ARGV[1])\n\
local window = tonumber(ARGV[2])\n\
local limit = tonumber(ARGV[3])\n\
local id = ARGV[4]\n\
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)\n\
local count = redis.call('ZCARD', key)\n\
if count < limit then\n\
redis.call('ZADD', key, now, id)\n\
redis.call('PEXPIRE', key, window)\n\
return 1\n\
end\n\
return 0\n";
let mut store = Store::new();
let _ = kevy::dispatch(
&mut store,
&argv(&[
b"ZADD",
b"rl:k",
b"1700000000001",
b"a",
b"1700000000002",
b"b",
]),
);
let reply = kevy::dispatch(
&mut store,
&argv(&[
b"EVAL",
SCRIPT,
b"1",
b"rl:k",
b"1700000000003",
b"60000",
b"2", b"req-3",
]),
);
assert_eq!(reply, b":0\r\n");
}