use revision::revisioned;
use surrealdb_datastore::values::session::DurableSession;
use surrealdb_expr::val::{Object, Value};
use surrealdb_iam::Auth;
use surrealdb_rpc::capabilities::NewPlannerStrategy;
use uuid::Uuid;
use crate::key::KVValue;
#[revisioned(revision = 1)]
#[derive(Clone, Debug, PartialEq)]
struct DurableSessionV1 {
expires_at: u64,
au: Auth,
rt: bool,
ip: Option<String>,
or: Option<String>,
id: Option<Uuid>,
ns: Option<String>,
db: Option<String>,
ac: Option<String>,
tk: Option<Value>,
rd: Option<Value>,
exp: Option<i64>,
variables: Object,
new_planner_strategy: NewPlannerStrategy,
redact_volatile_explain_attrs: bool,
}
fn durable_session_v1() -> DurableSessionV1 {
let mut variables = Object::default();
variables.insert("greeting", Value::from("hello"));
let mut token = Object::default();
token.insert("iss", Value::from("surrealdb"));
DurableSessionV1 {
expires_at: 123_456_789,
au: Auth::for_record("person:tobie".to_owned(), "app", "app", "account"),
rt: true,
ip: Some("10.0.0.1".to_owned()),
or: Some("example.com".to_owned()),
id: Some(Uuid::from_u128(7)),
ns: Some("app".to_owned()),
db: Some("app".to_owned()),
ac: Some("account".to_owned()),
tk: Some(Value::Object(token)),
rd: Some(Value::RecordId(surrealdb_expr::val::RecordId::new(
"person".into(),
surrealdb_strand::Strand::new("tobie"),
))),
exp: Some(1_700_000_000),
variables,
new_planner_strategy: NewPlannerStrategy::default(),
redact_volatile_explain_attrs: true,
}
}
#[rustfmt::skip]
const DURABLE_SESSION_V1: &[u8] = &[
1, 252, 21, 205, 91, 7, 1, 1, 1, 12, 112, 101, 114, 115, 111, 110,
58, 116, 111, 98, 105, 101, 5, 20, 1, 4, 3, 97, 112, 112, 3, 97,
112, 112, 7, 97, 99, 99, 111, 117, 110, 116, 0, 1, 1, 8, 49, 48,
46, 48, 46, 48, 46, 49, 1, 11, 101, 120, 97, 109, 112, 108, 101, 46,
99, 111, 109, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 7, 1, 3, 97, 112, 112, 1, 3, 97, 112, 112, 1, 7,
97, 99, 99, 111, 117, 110, 116, 1, 2, 74, 27, 0, 0, 0, 2, 22,
0, 0, 0, 0, 1, 3, 105, 115, 115, 2, 68, 10, 0, 0, 0, 9,
115, 117, 114, 114, 101, 97, 108, 100, 98, 1, 2, 78, 16, 0, 0, 0,
1, 6, 112, 101, 114, 115, 111, 110, 1, 1, 5, 116, 111, 98, 105, 101,
1, 252, 0, 226, 167, 202, 2, 23, 0, 0, 0, 0, 1, 8, 103, 114,
101, 101, 116, 105, 110, 103, 2, 68, 6, 0, 0, 0, 5, 104, 101, 108,
108, 111, 1, 0, 1,
];
#[test]
fn v1_durable_session_decodes_with_no_context_payload() {
let decoded = DurableSession::kv_decode_value(DURABLE_SESSION_V1, ()).unwrap_or_else(|e| {
panic!(
"BACKWARDS COMPATIBILITY BROKEN: revision-1 DurableSession bytes no longer decode.\n\
Error: {e}\n\n\
Every `/!se{{id}}` entry written by a previous version is revision 1. A node that \
cannot decode them drops or errors every attached session on upgrade."
)
});
let expected = durable_session_v1();
assert_eq!(decoded.data, None, "the field added at revision 2 must default to None");
assert_eq!(decoded.expires_at, expected.expires_at);
assert_eq!(decoded.au, expected.au);
assert_eq!(decoded.rt, expected.rt);
assert_eq!(decoded.ip, expected.ip);
assert_eq!(decoded.or, expected.or);
assert_eq!(decoded.id, expected.id);
assert_eq!(decoded.ns, expected.ns);
assert_eq!(decoded.db, expected.db);
assert_eq!(decoded.ac, expected.ac);
assert_eq!(decoded.tk, expected.tk);
assert_eq!(decoded.rd, expected.rd);
assert_eq!(decoded.exp, expected.exp);
assert_eq!(decoded.variables, expected.variables);
assert_eq!(decoded.new_planner_strategy, expected.new_planner_strategy);
assert_eq!(decoded.redact_volatile_explain_attrs, expected.redact_volatile_explain_attrs);
}
#[test]
fn re_encoding_an_upgraded_session_writes_revision_2_losslessly() {
let decoded = DurableSession::kv_decode_value(DURABLE_SESSION_V1, ()).expect("decode");
let re_encoded = decoded.kv_encode_value().expect("encode");
assert_eq!(DURABLE_SESSION_V1.first(), Some(&1), "the frozen fixture must be revision 1");
assert_eq!(re_encoded.first(), Some(&2), "an upgraded node must write revision 2");
assert_eq!(
re_encoded.len(),
DURABLE_SESSION_V1.len() + 1,
"the only added byte is the absent payload's `None` marker"
);
let round_tripped = DurableSession::kv_decode_value(&re_encoded, ()).expect("re-decode");
assert_eq!(round_tripped, decoded);
}
#[test]
#[ignore = "prints the frozen fixture; run by hand when the revision-1 shape is restated"]
fn generate() {
let bytes = revision::to_vec(&durable_session_v1()).expect("encode");
let body = bytes
.chunks(16)
.map(|row| {
let cells: Vec<String> = row.iter().map(|b| b.to_string()).collect();
format!("\t{},", cells.join(", "))
})
.collect::<Vec<_>>()
.join("\n");
println!("const DURABLE_SESSION_V1: &[u8] = &[\n{body}\n];");
}